From c423fb7ced41e1abd35db1fb09fe45d91e6b5ef2 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Wed, 31 Aug 2016 10:05:23 +0300 Subject: [PATCH 01/24] Update oled.js --- oled.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/oled.js b/oled.js index c8fbf17..920b10b 100644 --- a/oled.js +++ b/oled.js @@ -1,6 +1,4 @@ -var i2c = require('i2c'); - -var Oled = function(opts) { +var Oled = function(i2c, opts) { this.HEIGHT = opts.height || 32; this.WIDTH = opts.width || 128; @@ -66,7 +64,7 @@ var Oled = function(opts) { // Setup i2c console.log('this.ADDRESS: ' + this.ADDRESS); - this.wire = new i2c(this.ADDRESS, {device: '/dev/i2c-0'}); // point to your i2c address, debug provides REPL interface + this.wire = i2c; var screenSize = this.WIDTH + 'x' + this.HEIGHT; this.screenConfig = config[screenSize]; @@ -117,18 +115,18 @@ Oled.prototype._transfer = function(type, val, fn) { // send control and actual val // this.board.io.i2cWrite(this.ADDRESS, [control, val]); - this.wire.writeByte(control, function(err) { - this.wire.writeByte(val, function(err) { - fn(); - }); + this.wire.i2cWrite(this.ADDRESS, 2, new Buffer([control, val]), function(err) { + //this.wire.i2cWrite(val, function(err) { + fn(); + //}); }); } // read a byte from the oled Oled.prototype._readI2C = function(fn) { - this.wire.readByte(function(err, data) { + this.wire.i2cRead(this.ADDRESS, new Buffer(), 0, function(err, bytesRead, data) { // result is single byte - fn(data); + fn(data[0]); }); } From 4d529d756625996ba5f46f71c712edf881e32e11 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Wed, 31 Aug 2016 10:09:01 +0300 Subject: [PATCH 02/24] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 866c0d2..74ba309 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ OLED screens are really cool - now you can control them with JavaScript! If you haven't already, install [NodeJS](http://nodejs.org/). -`npm install oled-js-pi` +`npm install oled-i2c-bus` ## I2C screens Hook up I2C compatible oled to the Raspberry Pi. Pins: SDL and SCL @@ -25,7 +25,9 @@ Hook up I2C compatible oled to the Raspberry Pi. Pins: SDL and SCL ### I2C example ```javascript -var oled = require('oled-js-pi'); +var i2c = require('i2c-bus'), + i2cBus = i2c.openSync(1), + oled = require('oled-i2c-bus'); var opts = { width: 128, @@ -33,7 +35,7 @@ var opts = { address: 0x3D }; -var oled = new oled(opts); +var oled = new oled(i2cBus, opts); // do cool oled things here From 7ea6fc3ee37b05dc59b6251181ab14fef1765f47 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Wed, 31 Aug 2016 10:12:10 +0300 Subject: [PATCH 03/24] Update package.json --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 6b43e40..1010d43 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { - "name": "oled-js-pi", - "version": "1.0.5", + "name": "oled-i2c-bus", + "version": "1.0.6", "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 OLED screens)", "main": "oled.js", - "dependencies": {"i2c": "~0.2.1"}, "devDependencies": { "oled-font-5x7": "~1.0.0", "png-to-lcd": "~1.0.2", @@ -15,19 +14,20 @@ }, "repository": { "type": "git", - "url": "https://github.com/kd7yva/oled-js-pi.git" + "url": "https://github.com/baltazorr/oled-i2c-bus.git" }, "keywords": [ "Raspbery Pi", "Pi", "NodeJS", "oled", - "SSD1306" + "SSD1306", + "128x64" ], - "author": ["Judd Flamm", "Suz Hinton"], + "author": ["Judd Flamm", "Suz Hinton", "Bogdan Symchych"], "license": "MIT", "bugs": { - "url": "https://github.com/kd7yva/oled-js-pi/issues" + "url": "https://github.com/kd7yva/oled-i2c-bus/issues" }, - "homepage": "https://github.com/kd7yva/oled-js-pi" + "homepage": "https://github.com/baltazorr/oled-i2c-bus" } From 984c642a72347bb6967547b3b88a5f37b7b9006c Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Thu, 1 Sep 2016 19:45:00 +0300 Subject: [PATCH 04/24] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 74ba309..d4a62c5 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ OLED JS Pi ======================== -![oled-cat](http://f.cl.ly/items/2G041X2C1o2A1n2D3S18/cat-oled.png) - ## What is this? +This is fork of package [`oled-js-pi`](https://github.com/kd7yva/oled-js-pi) that works thru `i2c-bus` package and not use package `i2c`. + A NodeJS driver for I2C/SPI compatible monochrome OLED screens; to be used on the Raspberry Pi! Works with 128 x 32, 128 x 64 and 96 x 16 sized screens, of the SSD1306 OLED/PLED Controller (read the [datasheet here](http://www.adafruit.com/datasheets/SSD1306.pdf)). This based on the Blog Post and code by Suz Hinton - [Read her blog post about how OLED screens work](http://meow.noopkat.com/oled-js/)! From 0899562bbdae895857075f2a585c6e5fe62b9cd7 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Thu, 1 Sep 2016 19:48:24 +0300 Subject: [PATCH 05/24] Update oled.js --- oled.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/oled.js b/oled.js index 920b10b..ae161ec 100644 --- a/oled.js +++ b/oled.js @@ -116,15 +116,16 @@ Oled.prototype._transfer = function(type, val, fn) { // send control and actual val // this.board.io.i2cWrite(this.ADDRESS, [control, val]); this.wire.i2cWrite(this.ADDRESS, 2, new Buffer([control, val]), function(err) { - //this.wire.i2cWrite(val, function(err) { - fn(); - //}); + //TODO: why fn is undefined? + if(fn) { + fn(); + } }); } // read a byte from the oled Oled.prototype._readI2C = function(fn) { - this.wire.i2cRead(this.ADDRESS, new Buffer(), 0, function(err, bytesRead, data) { + this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function(err, bytesRead, data) { // result is single byte fn(data[0]); }); From de017709a6fa29ecc03169d664a194d6d07a507e Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Thu, 1 Sep 2016 19:49:10 +0300 Subject: [PATCH 06/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4a62c5..328e0ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![‘npm version’](http://img.shields.io/npm/v/oled-js.svg?style=flat) ![‘downloads over month’](http://img.shields.io/npm/dm/oled-js.svg?style=flat) -OLED JS Pi +OLED JS Pi over i2c-bus ======================== ## What is this? From e424ad9a8fe82417dc1ef747b9b69427b77ebecc Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 15 Nov 2016 11:22:31 +0300 Subject: [PATCH 07/24] Error fix fix for TypeError: "callback" argument must be a function --- oled.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oled.js b/oled.js index ae161ec..2241d7b 100644 --- a/oled.js +++ b/oled.js @@ -151,7 +151,7 @@ Oled.prototype._waitUntilReady = function(callback) { }); }; - setTimeout(tick(callback), 0); + setTimeout(() => { tick(callback) }, 0); } // set starting position of a text string on the oled From 606fa05f857894e598b4e2466e8ff35689225aa9 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 15 Nov 2016 12:51:55 +0300 Subject: [PATCH 08/24] Update oled.js --- oled.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oled.js b/oled.js index 2241d7b..791e924 100644 --- a/oled.js +++ b/oled.js @@ -151,7 +151,7 @@ Oled.prototype._waitUntilReady = function(callback) { }); }; - setTimeout(() => { tick(callback) }, 0); + setTimeout(function () { tick(callback) }, 0); } // set starting position of a text string on the oled From 33ebbd12bfb7b6e2947a086748fa9ba9239ba1a2 Mon Sep 17 00:00:00 2001 From: Harald Kubota Date: Sat, 26 Nov 2016 16:42:15 +0900 Subject: [PATCH 09/24] Made it work with NodeJS 6.9.1 on arm7l (Orange Pi) --- oled.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/oled.js b/oled.js index 791e924..c65c065 100644 --- a/oled.js +++ b/oled.js @@ -39,7 +39,7 @@ var Oled = function(i2c, opts) { this.cursor_y = 0; // new blank buffer - this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); + this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); this.buffer.fill(0x00); this.dirtyBytes = []; @@ -63,7 +63,7 @@ var Oled = function(i2c, opts) { }; // Setup i2c - console.log('this.ADDRESS: ' + this.ADDRESS); + //console.log('this.ADDRESS: ' + this.ADDRESS); this.wire = i2c; var screenSize = this.WIDTH + 'x' + this.HEIGHT; @@ -115,8 +115,9 @@ Oled.prototype._transfer = function(type, val, fn) { // send control and actual val // this.board.io.i2cWrite(this.ADDRESS, [control, val]); - this.wire.i2cWrite(this.ADDRESS, 2, new Buffer([control, val]), function(err) { - //TODO: why fn is undefined? + this.wire.i2cWrite(this.ADDRESS, 2, Buffer.from([control, val]), function(err) { + // Q: why fn is undefined? + // A: because _transfer() is called with 2 arguments if(fn) { fn(); } @@ -125,10 +126,9 @@ Oled.prototype._transfer = function(type, val, fn) { // read a byte from the oled Oled.prototype._readI2C = function(fn) { - this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function(err, bytesRead, data) { - // result is single byte - fn(data[0]); - }); + var data=[0]; + this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); + fn(data[0]); } // sometimes the oled gets a bit busy with lots of bytes. @@ -145,13 +145,12 @@ Oled.prototype._waitUntilReady = function(callback) { // if not busy, it's ready for callback callback(); } else { - console.log('I\'m busy!'); setTimeout(tick, 0); } }); }; - setTimeout(function () { tick(callback) }, 0); + setTimeout(function(){tick(callback)}, 0); } // set starting position of a text string on the oled From e9cacb69efb032bfdc9a0736797902fe1ded7431 Mon Sep 17 00:00:00 2001 From: Harald Kubota Date: Sat, 26 Nov 2016 20:46:04 +0900 Subject: [PATCH 10/24] Adding sample: clock --- examples/index.js | 58 +++++++++++++++++++++++++++++++++++++++++++ examples/package.json | 24 ++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 examples/index.js create mode 100644 examples/package.json diff --git a/examples/index.js b/examples/index.js new file mode 100644 index 0000000..e0c2a6b --- /dev/null +++ b/examples/index.js @@ -0,0 +1,58 @@ +"use strict"; + +var i2c = require('i2c-bus'), + i2cBus = i2c.openSync(0), + oled = require('oled-i2c-bus'); + +const SIZE_X=128, + SIZE_Y=64; + +var opts = { + width: SIZE_X, + height: SIZE_Y, + address: 0x3C +}; + +var oled = new oled(i2cBus, opts); + + +oled.clearDisplay(); +oled.turnOnDisplay(); + +oled.drawPixel([ + [SIZE_X-1, 0, 1], + [SIZE_X-1, SIZE_Y-1, 1], + [0, SIZE_Y-1, 1], + [0, 0, 1] +]); +oled.drawLine(1, 1, SIZE_X-2, 1, 1); +oled.drawLine(SIZE_X-2, 1, SIZE_X-2, SIZE_Y-2, 1); +oled.drawLine(SIZE_X-2, SIZE_Y-2, 1, SIZE_Y-2, 1); +oled.drawLine(1, SIZE_Y-2, 1, 1, 1); + +var font = require('oled-font-5x7'); + +// Clock + +function displayClock() { + var date=new Date(); + var hour = date.getHours(); + hour = (hour < 10 ? "0" : "") + hour; + var min = date.getMinutes(); + min = (min < 10 ? "0" : "") + min; + var sec = date.getSeconds(); + sec = (sec < 10 ? "0" : "") + sec; + + var year = date.getFullYear(); + var month = date.getMonth() + 1; + month = (month < 10 ? "0" : "") + month; + var day = date.getDate(); + day = (day < 10 ? "0" : "") + day; + + // Location fits 128x64 OLED + oled.setCursor(12, 25); + oled.writeString(font, 2, hour+":"+min+":"+sec, 1), true; +} + +setInterval(displayClock, 1000); + diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..b1b1e37 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,24 @@ +{ + "name": "test-oled", + "version": "1.0.0", + "description": "Testing OLED SSD1306 display with nodeJS", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/haraldkubota/oled-i2c-bus" + }, + "keywords": [ + "oled", + "i2c" + ], + "author": "Harald Kubota", + "license": "MIT", + "dependencies": { + "i2c-bus": "^1.1.2", + "oled-font-5x7": "^1.0.0", + "oled-i2c-bus": "^1.0.8" + } +} From b0ee2710ed67292b8466d735ca7f5d67bbb715e6 Mon Sep 17 00:00:00 2001 From: Harald Kubota Date: Mon, 28 Nov 2016 21:19:53 +0900 Subject: [PATCH 11/24] Adding README.md and cleaned up example clock program --- examples/README.md | 15 +++++++++++++++ examples/{index.js => clock.js} | 15 ++++++++++++--- examples/package.json | 4 ++-- 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 examples/README.md rename examples/{index.js => clock.js} (81%) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..00d8e18 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,15 @@ + +OLED JS Pi over i2c-bus +======================== + +## What is this directory? + +This directory contains a working example of NodeJS, a (typically) ARM board (Orange Pi Zero), and a 128x64 I2C SSD1306 display. + +## Install + +``` +npm install +cp ../oled.js node_modules/oled-i2c-bus/ +node clock.js +``` diff --git a/examples/index.js b/examples/clock.js similarity index 81% rename from examples/index.js rename to examples/clock.js index e0c2a6b..e93b2a7 100644 --- a/examples/index.js +++ b/examples/clock.js @@ -1,8 +1,16 @@ +/* + * clock.js + * Display a digital clock on a small I2C connected display + * + * 2016-11-28 v1.0 Harald Kubota + */ + + "use strict"; var i2c = require('i2c-bus'), - i2cBus = i2c.openSync(0), - oled = require('oled-i2c-bus'); + i2cBus = i2c.openSync(0), + oled = require('oled-i2c-bus'); const SIZE_X=128, SIZE_Y=64; @@ -25,6 +33,7 @@ oled.drawPixel([ [0, SIZE_Y-1, 1], [0, 0, 1] ]); + oled.drawLine(1, 1, SIZE_X-2, 1, 1); oled.drawLine(SIZE_X-2, 1, SIZE_X-2, SIZE_Y-2, 1); oled.drawLine(SIZE_X-2, SIZE_Y-2, 1, SIZE_Y-2, 1); @@ -51,7 +60,7 @@ function displayClock() { // Location fits 128x64 OLED oled.setCursor(12, 25); - oled.writeString(font, 2, hour+":"+min+":"+sec, 1), true; + oled.writeString(font, 2, hour+":"+min+":"+sec, 1, true); } setInterval(displayClock, 1000); diff --git a/examples/package.json b/examples/package.json index b1b1e37..b652eca 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,8 +1,8 @@ { - "name": "test-oled", + "name": "examples", "version": "1.0.0", "description": "Testing OLED SSD1306 display with nodeJS", - "main": "index.js", + "main": "clock.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, From 21fd9ada47db438bbce95de9e4f27f2cb87e4f26 Mon Sep 17 00:00:00 2001 From: Baltazor Date: Tue, 29 Nov 2016 18:52:27 +0200 Subject: [PATCH 12/24] Compatibility with older version as Node.js 6.0.0 --- oled.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/oled.js b/oled.js index c65c065..8601fc4 100644 --- a/oled.js +++ b/oled.js @@ -39,7 +39,12 @@ var Oled = function(i2c, opts) { this.cursor_y = 0; // new blank buffer - this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); + if(typeof Buffer.alloc == "undefined") { + this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); + } + else { + this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); + } this.buffer.fill(0x00); this.dirtyBytes = []; @@ -113,9 +118,17 @@ Oled.prototype._transfer = function(type, val, fn) { return; } + var bufferForSend; + if(typeof Buffer.from == "undefined") { + bufferForSend = new Buffer([control, val]); + } + else { + bufferForSend = Buffer.from([control, val]) + } + // send control and actual val // this.board.io.i2cWrite(this.ADDRESS, [control, val]); - this.wire.i2cWrite(this.ADDRESS, 2, Buffer.from([control, val]), function(err) { + this.wire.i2cWrite(this.ADDRESS, 2, bufferForSend, function(err) { // Q: why fn is undefined? // A: because _transfer() is called with 2 arguments if(fn) { @@ -126,9 +139,18 @@ Oled.prototype._transfer = function(type, val, fn) { // read a byte from the oled Oled.prototype._readI2C = function(fn) { - var data=[0]; - this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); - fn(data[0]); + + if(typeof Buffer.from == "undefined") { + this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function(err, bytesRead, data) { + // result is single byte + fn(data[0]); + }); + } + else { + var data=[0]; + this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); + fn(data[0]); + } } // sometimes the oled gets a bit busy with lots of bytes. @@ -150,7 +172,7 @@ Oled.prototype._waitUntilReady = function(callback) { }); }; - setTimeout(function(){tick(callback)}, 0); + setTimeout(function () {tick(callback) }, 0); } // set starting position of a text string on the oled From 52fda011228fc47f378225d18730c02c9d6c3c85 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Tue, 29 Nov 2016 19:19:58 +0200 Subject: [PATCH 13/24] Compatibility with older version as Node.js 6.0.0 --- oled.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/oled.js b/oled.js index 8601fc4..b57b334 100644 --- a/oled.js +++ b/oled.js @@ -39,9 +39,11 @@ var Oled = function(i2c, opts) { this.cursor_y = 0; // new blank buffer + //For version <6.0.0 if(typeof Buffer.alloc == "undefined") { this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); } + //For version >=6.0.0 else { this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); } @@ -119,9 +121,11 @@ Oled.prototype._transfer = function(type, val, fn) { } var bufferForSend; + //For version <6.0.0 if(typeof Buffer.from == "undefined") { bufferForSend = new Buffer([control, val]); } + //For version >=6.0.0 else { bufferForSend = Buffer.from([control, val]) } @@ -139,13 +143,19 @@ Oled.prototype._transfer = function(type, val, fn) { // read a byte from the oled Oled.prototype._readI2C = function(fn) { - + //For version <6.0.0 if(typeof Buffer.from == "undefined") { this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function(err, bytesRead, data) { // result is single byte - fn(data[0]); + if(typeof data === "object") { + fn(data[0]); + } + else { + fn(0); + } }); } + //For version >=6.0.0 else { var data=[0]; this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); From 4da48ce37ad1e832887521e4e7e69626eff6b4b3 Mon Sep 17 00:00:00 2001 From: Bogdan Symchych Date: Tue, 29 Nov 2016 19:22:02 +0200 Subject: [PATCH 14/24] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1010d43..e9f8ab2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oled-i2c-bus", - "version": "1.0.6", + "version": "1.0.9", "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 OLED screens)", "main": "oled.js", "devDependencies": { From 3ed12ab28c7c6825ab1d856b690f6e6ada94bcfe Mon Sep 17 00:00:00 2001 From: Bryan Nielsen Date: Tue, 4 Jul 2017 12:25:05 -0700 Subject: [PATCH 15/24] Fix setTimeout bug in busy check. Fix bug in writeString that adds extra space at end of displayed string. --- oled.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/oled.js b/oled.js index b57b334..afaa790 100644 --- a/oled.js +++ b/oled.js @@ -177,7 +177,7 @@ Oled.prototype._waitUntilReady = function(callback) { // if not busy, it's ready for callback callback(); } else { - setTimeout(tick, 0); + setTimeout(function () {tick(callback) }, 0); } }); }; @@ -202,8 +202,10 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { // loop through words for (var w = 0; w < len; w += 1) { - // put the word space back in - wordArr[w] += ' '; + // 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)); From 53d938ab9fc6dae2b6dff500b98469bab40c9652 Mon Sep 17 00:00:00 2001 From: Bryan Nielsen Date: Thu, 6 Jul 2017 10:51:41 -0700 Subject: [PATCH 16/24] Added linespacing and letterspacing as options and fixed bugs in offset and linespacing on wrap. --- oled.js | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/oled.js b/oled.js index afaa790..67004d8 100644 --- a/oled.js +++ b/oled.js @@ -4,6 +4,8 @@ var Oled = function(i2c, opts) { this.WIDTH = opts.width || 128; this.ADDRESS = opts.address || 0x3C; 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; @@ -198,7 +200,7 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { len = wordArr.length, // start x offset at cursor pos offset = this.cursor_x, - padding = 0, letspace = 1, leading = 2; + padding = 0; // loop through words for (var w = 0; w < len; w += 1) { @@ -212,31 +214,38 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { // wrap words if necessary if (wrap && len > 1 && (offset >= (this.WIDTH - compare)) ) { - offset = 1; - this.cursor_y += (font.height * size) + size + leading; + 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) { - // 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); - // draw the entire character - this._drawChar(charBytes, 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 : size + letspace; - offset += (font.width * size) + padding; - - // wrap letters if necessary - if (wrap && (offset >= (this.WIDTH - font.width - letspace))) { - offset = 1; - this.cursor_y += (font.height * size) + size + leading; + if (stringArr[i] === '\n') { + offset = 0; + this.cursor_y += (font.height * size) + this.LINESPACING; + } + 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); + // draw the entire character + this._drawChar(charBytes, 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); } - // set the 'cursor' for the next char to be drawn, then loop again for next char - this.setCursor(offset, this.cursor_y); } } if (immed) { From dea312d670af8ba5edf564f39c0010885020c020 Mon Sep 17 00:00:00 2001 From: Bryan Nielsen Date: Sat, 12 Aug 2017 18:33:16 -0700 Subject: [PATCH 17/24] Fix bug where writes to display could start before asynchronous initialization was complete. Fixed bug in draw pixel wrapping due to incorrect limits check. Fixed bug in line wrapping when writing strings. --- oled.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/oled.js b/oled.js index 67004d8..8fd5496 100644 --- a/oled.js +++ b/oled.js @@ -122,7 +122,7 @@ Oled.prototype._transfer = function(type, val, fn) { return; } - var bufferForSend; + var bufferForSend, sentCount; //For version <6.0.0 if(typeof Buffer.from == "undefined") { bufferForSend = new Buffer([control, val]); @@ -133,14 +133,10 @@ Oled.prototype._transfer = function(type, val, fn) { } // send control and actual val - // this.board.io.i2cWrite(this.ADDRESS, [control, val]); - this.wire.i2cWrite(this.ADDRESS, 2, bufferForSend, function(err) { - // Q: why fn is undefined? - // A: because _transfer() is called with 2 arguments - if(fn) { - fn(); - } - }); + sentCount = this.wire.i2cWriteSync(this.ADDRESS, 2, bufferForSend); + if(fn) { + fn(); + } } // read a byte from the oled @@ -225,6 +221,7 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { 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 @@ -419,7 +416,7 @@ Oled.prototype.drawPixel = function(pixels, sync) { 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; + if (x >= this.WIDTH || y >= this.HEIGHT) return; // thanks, Martin Richards. // I wanna can this, this tool is for devs who get 0 indexes From d5b9686235099e8b083a756c5d10a79ab6f0a552 Mon Sep 17 00:00:00 2001 From: laurbadescu <32358639+laurbadescu@users.noreply.github.com> Date: Sun, 18 Feb 2018 20:20:43 +0200 Subject: [PATCH 18/24] Speed up page update on display and page clear I am using oled-i2c-bus to display an interactive menu on a SSD1306 0.96 128x64 OLED display attached to a Raspberry Pi Zero. The library works very well except for a few design flaws that can be easily corrected. The most important one is the display speed. By using _transfer function, the payload is segmented into individual bytes that are send each in a separate transaction. These transactions add the data byte (0x40) before the payload byte thus doubling the throughput. This has the unpleasant effect of one actually seeing the screen as it slowly refreshes. However, the payload bytes can also be sent in a burst, with a single leading data byte 0x40, bypassing _transfer function altogether and writing directly to i2c. Therefore the update time is halved. This doesn't sound as much in theory but in practice is a huge improvement as the eye sees an almost instant update as opposed to a slooooow refresh. Another improvement would be the actual clearDisplay function which is very complicated and slow. A clear display action should simply fill the buffer with 0x00 and if required, update the display. --- oled.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/oled.js b/oled.js index 8fd5496..38b7959 100644 --- a/oled.js +++ b/oled.js @@ -330,9 +330,8 @@ Oled.prototype.update = function() { } // write buffer data - for (v = 0; v < bufferLen; v += 1) { - this._transfer('data', this.buffer[v]); - } + var bufferToSend = Buffer.concat([Buffer.from([0x40]), this.buffer]); + var sentCount = this.wire.i2cWriteSync(this.ADDRESS, bufferToSend.length, bufferToSend); }.bind(this)); } @@ -365,18 +364,11 @@ Oled.prototype.turnOnDisplay = function() { Oled.prototype.clearDisplay = function(sync) { var immed = (typeof sync === 'undefined') ? true : sync; // write off pixels - //this.buffer.fill(0x00); - for (var i = 0; i < this.buffer.length; i += 1) { - if (this.buffer[i] !== 0x00) { - this.buffer[i] = 0x00; - if (this.dirtyBytes.indexOf(i) === -1) { - this.dirtyBytes.push(i); - } - } - } - if (immed) { - this._updateDirtyBytes(this.dirtyBytes); - } + this.buffer.fill(0x00); + if (immed) { + this.update(); + } + } // invert pixels on oled From 6d747780508fba1cfa152ae5dcedb79095ea0cdb Mon Sep 17 00:00:00 2001 From: Bryan Nielsen Date: Sun, 19 Aug 2018 17:40:26 -0700 Subject: [PATCH 19/24] Added drawRGBAImage() method with examples and documentation. --- README.md | 42 +++++++++++++++++++++++++++ examples/package.json | 3 +- examples/rgba.js | 39 +++++++++++++++++++++++++ examples/test.png | Bin 0 -> 648 bytes oled.js | 65 ++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++- 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 examples/rgba.js create mode 100644 examples/test.png diff --git a/README.md b/README.md index 328e0ee..316f76a 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,48 @@ pngtolcd('nyan-cat.png', true, function(err, bitmap) { }); ``` +### drawRGBAImage +Draw an RGBA coded image at specific coordinates. This only supports a monochrome +OLED so transparent pixels must be 100% transparent, off pixels should have an +RGB value of (0, 0, 0), and pixels with any color value will be considered on. + +Use a library such as [pngjs](https://www.npmjs.com/package/pngjs) to read a png +file into the required rgba data structure. + +Example: +```JavaScript +const fs = require('fs'); +const PNG = require('pngjs').PNG; +const i2c = require('i2c-bus'); +const oled = require('oled-i2c-bus'); + +var i2cBus = i2c.openSync(0); + +var opts = { + width: 128, + height: 64, + address: 0x3C +}; + +var display = new oled(i2cBus, opts); + +display.clearDisplay(); +display.turnOnDisplay(); + +fs.createReadStream('./test.png') +.pipe(new PNG({ filterType: 4 })) +.on('parsed', function () { + setInterval(() => { drawImage(this) }, 1000); +}); + +function drawImage(image) { + let x = Math.floor(Math.random() * (display.WIDTH) - image.width / 2); + let y = Math.floor(Math.random() * (display.HEIGHT) - image.height / 2); + display.drawRGBAImage(image, x, y); +} +``` + + ### startScroll Scrolls the current display either left or right. Arguments: diff --git a/examples/package.json b/examples/package.json index b652eca..2baf2a0 100644 --- a/examples/package.json +++ b/examples/package.json @@ -19,6 +19,7 @@ "dependencies": { "i2c-bus": "^1.1.2", "oled-font-5x7": "^1.0.0", - "oled-i2c-bus": "^1.0.8" + "oled-i2c-bus": "^1.0.11", + "pngjs": "^3.3.3" } } diff --git a/examples/rgba.js b/examples/rgba.js new file mode 100644 index 0000000..10f0b1d --- /dev/null +++ b/examples/rgba.js @@ -0,0 +1,39 @@ +/* + * rgba.js + * Display an RGBA image at random locations on a small I2C connected display + * + * 2018-08-19 v1.0 Bryan Nielsen + */ + + +"use strict"; + +const fs = require('fs'); +const PNG = require('pngjs').PNG; +const i2c = require('i2c-bus'); +const oled = require('../oled');// 'oled-i2c-bus'); + +var i2cBus = i2c.openSync(0); + +var opts = { + width: 128, + height: 64, + address: 0x3C +}; + +var display = new oled(i2cBus, opts); + +display.clearDisplay(); +display.turnOnDisplay(); + +fs.createReadStream('./test.png') +.pipe(new PNG({ filterType: 4 })) +.on('parsed', function () { + setInterval(() => { drawImage(this) }, 1000); +}); + +function drawImage(image) { + let x = Math.floor(Math.random() * (display.WIDTH) - image.width / 2); + let y = Math.floor(Math.random() * (display.HEIGHT) - image.height / 2); + display.drawRGBAImage(image, x, y); +} diff --git a/examples/test.png b/examples/test.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc585889573b119630040acc4cfa375fecdddc1 GIT binary patch literal 648 zcmV;30(bq1P)WFU8GbZ8()Nlj2>E@cM*00HbtL_t(Y$JLfUYg17e zho9SmV2UW=4i!4YCE6}#2)59nbj(ybICK)bIb=0i%upx&0xr^$$w9Y%0*5Xog>=(G zlO^IJ!6J8Sn(SKNdOz4uNCf6VL~~ z0^fii!0(WE1GojGz+K=T@BnxSJOXZy_52L9fj3E#d}orWv+7HAjupIBSD3P?D?ux>Q+4BFQfn;Xow_ukVKHoGNBy97)Gz9}`X~5# zSnGtGolsMp(03_lXVwBp)K|dskhh&A$zG)RCa?`W2G%;A4hIJZbh}+nPEHsM1`LNo zMxzk`g+hU1vB<*00_AdMGT072qTA9@tHi-DL@WnQn>Gst#?V-0mK5*Vtc zX*zYlG)*(WVT5CDhMe0BP>rnb18i+=O&wrNV?PqE{uduf?ys3#U)1^k)#ZIyxHxW^ i%W*5u18b7^k^e8)pT(41DCM{S0000= 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; diff --git a/package.json b/package.json index e9f8ab2..ead1ddb 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "oled-i2c-bus", - "version": "1.0.9", + "version": "1.0.11", "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 OLED screens)", "main": "oled.js", "devDependencies": { + "i2c-bus": "^4.0.1", "oled-font-5x7": "~1.0.0", "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", "pngparse": "~2.0.1", "temporal": "^0.3.8" }, From a7e2ada95f64192dcafc7789502183745d0f0c52 Mon Sep 17 00:00:00 2001 From: Trevor Gionet Date: Tue, 22 Jan 2019 15:41:14 -0500 Subject: [PATCH 20/24] readme update fillRect specifies width and height not lower right coordinates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 316f76a..8769f45 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Draws a filled rectangle. Arguments: + int **x0, y0** - top left corner of rectangle -+ int **x1, y1** - bottom right corner of rectangle ++ int **w, h** - width and height of rectangle + int **color** - can be specified as either 0 for 'off' or black, and 1 or 255 for 'on' or white. Optional bool as last argument specifies whether screen updates immediately with result. Default is true. From 63e57bef2e6aca114e53ec6d79da4cfa441df66d Mon Sep 17 00:00:00 2001 From: Leon Anavi Date: Mon, 14 Oct 2019 01:55:46 +0300 Subject: [PATCH 21/24] examples/clock.js: Failsafe checks for I2C bus Avoid ENOENT exception due to missing I2C bus. Add a variable for setting the I2C bus, by default 1 as this is vaild for newer Raspberry Pi versions. On other platforms the value might be different, for example 0. Tested on Raspberry Pi 4 Model B. Signed-off-by: Leon Anavi --- examples/clock.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/examples/clock.js b/examples/clock.js index e93b2a7..fefd3b9 100644 --- a/examples/clock.js +++ b/examples/clock.js @@ -8,8 +8,13 @@ "use strict"; +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1, +// however on other platforms you may need to adjust if to +// another value, for example 0. +var bus = 1; + var i2c = require('i2c-bus'), - i2cBus = i2c.openSync(0), + i2cBus = i2c.openSync(bus), oled = require('oled-i2c-bus'); const SIZE_X=128, @@ -21,23 +26,30 @@ var opts = { address: 0x3C }; -var oled = new oled(i2cBus, opts); +try { + var oled = new oled(i2cBus, opts); -oled.clearDisplay(); -oled.turnOnDisplay(); + oled.clearDisplay(); + oled.turnOnDisplay(); -oled.drawPixel([ + oled.drawPixel([ [SIZE_X-1, 0, 1], [SIZE_X-1, SIZE_Y-1, 1], [0, SIZE_Y-1, 1], [0, 0, 1] -]); + ]); -oled.drawLine(1, 1, SIZE_X-2, 1, 1); -oled.drawLine(SIZE_X-2, 1, SIZE_X-2, SIZE_Y-2, 1); -oled.drawLine(SIZE_X-2, SIZE_Y-2, 1, SIZE_Y-2, 1); -oled.drawLine(1, SIZE_Y-2, 1, 1, 1); + oled.drawLine(1, 1, SIZE_X-2, 1, 1); + oled.drawLine(SIZE_X-2, 1, SIZE_X-2, SIZE_Y-2, 1); + oled.drawLine(SIZE_X-2, SIZE_Y-2, 1, SIZE_Y-2, 1); + oled.drawLine(1, SIZE_Y-2, 1, 1, 1); +} +catch(err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} var font = require('oled-font-5x7'); From ed1c32a96e97bc4e873185bd527c5d0692cc72b4 Mon Sep 17 00:00:00 2001 From: Gaspard Beernaert Date: Mon, 23 Mar 2020 15:31:33 +0100 Subject: [PATCH 22/24] center vertically the clock so that it works on the first time! --- examples/clock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/clock.js b/examples/clock.js index fefd3b9..4007767 100644 --- a/examples/clock.js +++ b/examples/clock.js @@ -70,8 +70,7 @@ function displayClock() { var day = date.getDate(); day = (day < 10 ? "0" : "") + day; - // Location fits 128x64 OLED - oled.setCursor(12, 25); + oled.setCursor(12, Math.floor(SIZE_Y/2) + 7); oled.writeString(font, 2, hour+":"+min+":"+sec, 1, true); } From 673696081fc242cc9a1e27d03ecdc5eabfbefe7d Mon Sep 17 00:00:00 2001 From: lynniemagoo Date: Sun, 6 Dec 2020 15:29:12 -0600 Subject: [PATCH 23/24] Enhancement to add support for larger fonts. This change is backward compatible with the 1.0.11 version. I recently have constructed additional oled fonts from a site I found that had character sizes > 8 pixels. These fonts are useful for displays that are larger than 128x32 such as 128x64 or 128x128. I plan to add these fonts in the very near future to npm but before I do, I need a module that will support them. The current oled font character structure is such that each element in the array is a represents a column and bit values of the byte represent rows in the column. The current code supports font with character widths larger than 8 pixels but it does not support fonts with character heights larger than 8 pixels. This update includes a minor fix to word wrap (should no wrap the first word) and the changes to support fonts with character height larger than 8 pixels. Version number bumped to 1.0.12. --- oled.js | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/oled.js b/oled.js index b46ac5c..d4a6938 100644 --- a/oled.js +++ b/oled.js @@ -209,7 +209,7 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { compare = (font.width * size * slen) + (size * (len -1)); // wrap words if necessary - if (wrap && len > 1 && (offset >= (this.WIDTH - compare)) ) { + if (wrap && len > 1 && w > 0 && (offset >= (this.WIDTH - compare)) ) { offset = 0; this.cursor_y += (font.height * size) + this.LINESPACING; @@ -227,9 +227,9 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { // 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); + var charBytes = this._readCharBytes(charBuf, font.height); // draw the entire character - this._drawChar(charBytes, size, false); + 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; @@ -251,14 +251,14 @@ Oled.prototype.writeString = function(font, size, string, color, wrap, sync) { } // draw an individual character to the screen -Oled.prototype._drawChar = function(byteArray, size, sync) { +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 < 8; j += 1) { + for (var j = 0; j < charHeight; j += 1) { // pull color out var color = byteArray[i][j], xpos, ypos; @@ -278,7 +278,7 @@ Oled.prototype._drawChar = function(byteArray, size, sync) { } // get character bytes from the supplied font object in order to send to framebuffer -Oled.prototype._readCharBytes = function(byteArray) { +Oled.prototype._readCharBytes = function(byteArray, charHeight) { var bitArr = [], bitCharArr = []; // loop through each byte supplied for a char @@ -286,7 +286,7 @@ Oled.prototype._readCharBytes = function(byteArray) { // set current byte var byte = byteArray[i]; // read each byte - for (var j = 0; j < 8; j += 1) { + for (var j = 0; j < charHeight; j += 1) { // shift bits right until all are read var bit = byte >> j & 1; bitArr.push(bit); diff --git a/package.json b/package.json index ead1ddb..e911230 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oled-i2c-bus", - "version": "1.0.11", + "version": "1.0.12", "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 OLED screens)", "main": "oled.js", "devDependencies": { From f06dbdd5f94674665dc509182c51559125453cba Mon Sep 17 00:00:00 2001 From: Abdul Hadi Fikri Date: Thu, 4 May 2023 22:07:05 +0800 Subject: [PATCH 24/24] feature: add support for sh1106 - restructure the code to accomodate swappable drivers - add a few of new APIs to draw an interactive and indicators icons * battery * bluetooth * wifi * image (a wrapper for `drawRGBAImage`) + add some "png" files for icons, numbers and logos - use `oled-font-pack` in examples and tests - allow errors to bubble up * except for `_waitUntilReady` function which might cause uncaught exception - bump package version to 1.1.0 - update readme to include instructions on enabling software I2C and the new APIs --- README.md | 95 +++- drivers/sh1106.js | 659 ++++++++++++++++++++++ drivers/ssd1306.js | 663 ++++++++++++++++++++++ examples/README.md | 14 +- examples/clock.js | 78 --- examples/package-lock.json | 171 ++++++ examples/package.json | 12 +- examples/sh1106_clock.js | 68 +++ examples/{rgba.js => sh1106_rgba.js} | 9 +- examples/ssd1306_clock.js | 68 +++ examples/ssd1306_rgba.js | 39 ++ oled.js | 803 ++++++++------------------- package-lock.json | 185 ++++++ package.json | 21 +- resources/OledBattery.png | Bin 0 -> 232 bytes resources/OledBluetooth.png | Bin 0 -> 6567 bytes resources/OledClear.png | Bin 0 -> 217 bytes resources/OledCleardigit.png | Bin 0 -> 4781 bytes resources/OledClock.png | Bin 0 -> 5111 bytes resources/OledClockWhite.png | Bin 0 -> 5137 bytes resources/OledDim.png | Bin 0 -> 212 bytes resources/OledEight.png | Bin 0 -> 746 bytes resources/OledEightWhite.png | Bin 0 -> 7698 bytes resources/OledEightWhiteT.png | Bin 0 -> 625 bytes resources/OledFive.png | Bin 0 -> 626 bytes resources/OledFiveWhite.png | Bin 0 -> 7085 bytes resources/OledFiveWhiteT.png | Bin 0 -> 633 bytes resources/OledFour.png | Bin 0 -> 688 bytes resources/OledFourWhite.png | Bin 0 -> 6763 bytes resources/OledFourWhiteT.png | Bin 0 -> 634 bytes resources/OledImage.png | Bin 0 -> 365 bytes resources/OledInvert.png | Bin 0 -> 233 bytes resources/OledLine.png | Bin 0 -> 216 bytes resources/OledNine.png | Bin 0 -> 637 bytes resources/OledNineWhite.png | Bin 0 -> 7575 bytes resources/OledNineWhiteT.png | Bin 0 -> 7331 bytes resources/OledOne.png | Bin 0 -> 5965 bytes resources/OledOneWhite.png | Bin 0 -> 6667 bytes resources/OledOneWhiteT.png | Bin 0 -> 5387 bytes resources/OledPixel.png | Bin 0 -> 263 bytes resources/OledRectangle.png | Bin 0 -> 192 bytes resources/OledScroll.png | Bin 0 -> 380 bytes resources/OledSeven.png | Bin 0 -> 628 bytes resources/OledSevenWhite.png | Bin 0 -> 6633 bytes resources/OledSevenWhiteT.png | Bin 0 -> 636 bytes resources/OledSix.png | Bin 0 -> 623 bytes resources/OledSixWhite.png | Bin 0 -> 7650 bytes resources/OledSixWhiteT.png | Bin 0 -> 7402 bytes resources/OledString.png | Bin 0 -> 475 bytes resources/OledThree.png | Bin 0 -> 695 bytes resources/OledThreeWhite.png | Bin 0 -> 7013 bytes resources/OledThreeWhiteT.png | Bin 0 -> 620 bytes resources/OledTurnOff.png | Bin 0 -> 548 bytes resources/OledTurnOn.png | Bin 0 -> 192 bytes resources/OledTwo.png | Bin 0 -> 6795 bytes resources/OledTwoWhite.png | Bin 0 -> 6957 bytes resources/OledTwoWhiteT.png | Bin 0 -> 645 bytes resources/OledTwopoints.png | Bin 0 -> 578 bytes resources/OledTwopointsWhite.png | Bin 0 -> 587 bytes resources/OledWifi.png | Bin 0 -> 304 bytes resources/OledZero.png | Bin 0 -> 7266 bytes resources/OledZeroWhite.png | Bin 0 -> 7570 bytes resources/OledZeroWhiteT.png | Bin 0 -> 7314 bytes resources/Oledrpi32x32.png | Bin 0 -> 6242 bytes resources/Oledrpi48x48.png | Bin 0 -> 8807 bytes resources/Oledrpi64x64.png | Bin 0 -> 9798 bytes resources/WaterBrain.png | Bin 0 -> 3946 bytes resources/icon_128x64_kiss.png | Bin 0 -> 349 bytes resources/notafile.png | Bin 0 -> 6536 bytes resources/raspberrypi-logo.png | Bin 0 -> 10903 bytes resources/rpi frambuesa.png | Bin 0 -> 9798 bytes resources/rpi-frambuesa.png | Bin 0 -> 9798 bytes resources/yahabommlogo-2-102x16.png | Bin 0 -> 6835 bytes resources/yahabommlogo-2-128x20.png | Bin 0 -> 7550 bytes tests/readme.md | 3 + tests/sh1106/battery.js | 40 ++ tests/sh1106/bluetooth.js | 34 ++ tests/sh1106/control.js | 78 +++ tests/sh1106/drawBitmap.js | 39 ++ tests/sh1106/drawLine.js | 42 ++ tests/sh1106/drawPageSeg.js | 45 ++ tests/sh1106/drawPixel.js | 55 ++ tests/sh1106/drawRGBAImage.js | 56 ++ tests/sh1106/fillRect.js | 41 ++ tests/sh1106/image.js | 36 ++ tests/sh1106/package-lock.json | 171 ++++++ tests/sh1106/package.json | 27 + tests/sh1106/update.js | 47 ++ tests/sh1106/wifi.js | 40 ++ tests/sh1106/writeString.js | 47 ++ tests/ssd1306/battery.js | 38 ++ tests/ssd1306/bluetooth.js | 32 ++ tests/ssd1306/control.js | 82 +++ tests/ssd1306/drawBitmap.js | 37 ++ tests/ssd1306/drawLine.js | 40 ++ tests/ssd1306/drawPageSeg.js | 43 ++ tests/ssd1306/drawPixel.js | 53 ++ tests/ssd1306/drawRGBAImage.js | 54 ++ tests/ssd1306/fillRect.js | 39 ++ tests/ssd1306/image.js | 34 ++ tests/ssd1306/package-lock.json | 182 ++++++ tests/ssd1306/package.json | 27 + tests/ssd1306/wifi.js | 38 ++ tests/ssd1306/writeString.js | 44 ++ 104 files changed, 3754 insertions(+), 675 deletions(-) create mode 100644 drivers/sh1106.js create mode 100644 drivers/ssd1306.js delete mode 100644 examples/clock.js create mode 100644 examples/package-lock.json create mode 100644 examples/sh1106_clock.js rename examples/{rgba.js => sh1106_rgba.js} (90%) create mode 100644 examples/ssd1306_clock.js create mode 100644 examples/ssd1306_rgba.js create mode 100644 package-lock.json create mode 100644 resources/OledBattery.png create mode 100644 resources/OledBluetooth.png create mode 100644 resources/OledClear.png create mode 100644 resources/OledCleardigit.png create mode 100644 resources/OledClock.png create mode 100644 resources/OledClockWhite.png create mode 100644 resources/OledDim.png create mode 100644 resources/OledEight.png create mode 100644 resources/OledEightWhite.png create mode 100644 resources/OledEightWhiteT.png create mode 100644 resources/OledFive.png create mode 100644 resources/OledFiveWhite.png create mode 100644 resources/OledFiveWhiteT.png create mode 100644 resources/OledFour.png create mode 100644 resources/OledFourWhite.png create mode 100644 resources/OledFourWhiteT.png create mode 100644 resources/OledImage.png create mode 100644 resources/OledInvert.png create mode 100644 resources/OledLine.png create mode 100644 resources/OledNine.png create mode 100644 resources/OledNineWhite.png create mode 100644 resources/OledNineWhiteT.png create mode 100644 resources/OledOne.png create mode 100644 resources/OledOneWhite.png create mode 100644 resources/OledOneWhiteT.png create mode 100644 resources/OledPixel.png create mode 100644 resources/OledRectangle.png create mode 100644 resources/OledScroll.png create mode 100644 resources/OledSeven.png create mode 100644 resources/OledSevenWhite.png create mode 100644 resources/OledSevenWhiteT.png create mode 100644 resources/OledSix.png create mode 100644 resources/OledSixWhite.png create mode 100644 resources/OledSixWhiteT.png create mode 100644 resources/OledString.png create mode 100644 resources/OledThree.png create mode 100644 resources/OledThreeWhite.png create mode 100644 resources/OledThreeWhiteT.png create mode 100644 resources/OledTurnOff.png create mode 100644 resources/OledTurnOn.png create mode 100644 resources/OledTwo.png create mode 100644 resources/OledTwoWhite.png create mode 100644 resources/OledTwoWhiteT.png create mode 100644 resources/OledTwopoints.png create mode 100644 resources/OledTwopointsWhite.png create mode 100644 resources/OledWifi.png create mode 100644 resources/OledZero.png create mode 100644 resources/OledZeroWhite.png create mode 100644 resources/OledZeroWhiteT.png create mode 100644 resources/Oledrpi32x32.png create mode 100644 resources/Oledrpi48x48.png create mode 100644 resources/Oledrpi64x64.png create mode 100644 resources/WaterBrain.png create mode 100644 resources/icon_128x64_kiss.png create mode 100644 resources/notafile.png create mode 100644 resources/raspberrypi-logo.png create mode 100644 resources/rpi frambuesa.png create mode 100644 resources/rpi-frambuesa.png create mode 100644 resources/yahabommlogo-2-102x16.png create mode 100644 resources/yahabommlogo-2-128x20.png create mode 100644 tests/readme.md create mode 100644 tests/sh1106/battery.js create mode 100644 tests/sh1106/bluetooth.js create mode 100644 tests/sh1106/control.js create mode 100644 tests/sh1106/drawBitmap.js create mode 100644 tests/sh1106/drawLine.js create mode 100644 tests/sh1106/drawPageSeg.js create mode 100644 tests/sh1106/drawPixel.js create mode 100644 tests/sh1106/drawRGBAImage.js create mode 100644 tests/sh1106/fillRect.js create mode 100644 tests/sh1106/image.js create mode 100644 tests/sh1106/package-lock.json create mode 100644 tests/sh1106/package.json create mode 100644 tests/sh1106/update.js create mode 100644 tests/sh1106/wifi.js create mode 100644 tests/sh1106/writeString.js create mode 100644 tests/ssd1306/battery.js create mode 100644 tests/ssd1306/bluetooth.js create mode 100644 tests/ssd1306/control.js create mode 100644 tests/ssd1306/drawBitmap.js create mode 100644 tests/ssd1306/drawLine.js create mode 100644 tests/ssd1306/drawPageSeg.js create mode 100644 tests/ssd1306/drawPixel.js create mode 100644 tests/ssd1306/drawRGBAImage.js create mode 100644 tests/ssd1306/fillRect.js create mode 100644 tests/ssd1306/image.js create mode 100644 tests/ssd1306/package-lock.json create mode 100644 tests/ssd1306/package.json create mode 100644 tests/ssd1306/wifi.js create mode 100644 tests/ssd1306/writeString.js diff --git a/README.md b/README.md index 8769f45..76ac78d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ OLED JS Pi over i2c-bus This is fork of package [`oled-js-pi`](https://github.com/kd7yva/oled-js-pi) that works thru `i2c-bus` package and not use package `i2c`. -A NodeJS driver for I2C/SPI compatible monochrome OLED screens; to be used on the Raspberry Pi! Works with 128 x 32, 128 x 64 and 96 x 16 sized screens, of the SSD1306 OLED/PLED Controller (read the [datasheet here](http://www.adafruit.com/datasheets/SSD1306.pdf)). +A NodeJS driver for I2C/SPI compatible monochrome OLED screens; to be used on the Raspberry Pi! Works with 128 x 32, 128 x 64 and 96 x 16 sized screens, of the SSD1306/SH1106 OLED/PLED Controller (read the [datasheet here](http://www.adafruit.com/datasheets/SSD1306.pdf)). This based on the Blog Post and code by Suz Hinton - [Read her blog post about how OLED screens work](http://meow.noopkat.com/oled-js/)! @@ -15,26 +15,43 @@ OLED screens are really cool - now you can control them with JavaScript! ## Install +Raspberry Pi allows for software I2C. To enable software I2C, add `dtoverlay=i2c-gpio,bus=3` to `/boot.config.txt`. The software I2C would be available on `bus` no `3` +where the `SDA` is on pin `GPIO23`/`BCM 16` and `SCK` is on pun `GPIO24`/`BCM 18`. + If you haven't already, install [NodeJS](http://nodejs.org/). `npm install oled-i2c-bus` +For `SH1106`, if you get an error: +``` +"Error: , Remote I/O error" +``` + +You might have to lower the baudrate by adding the following line to `/boot/config.txt` and rebooting the Pi +``` +dtparam=i2c_baudrate=10000 +``` + +This is a known issue with Raspberry Pi as noted in [Raspberry Pi I2C hardware bug](https://github.com/fivdi/i2c-bus/issues/36). Alternatively, use software I2C. + ## I2C screens -Hook up I2C compatible oled to the Raspberry Pi. Pins: SDL and SCL +Hook up I2C compatible oled to the Raspberry Pi. Pins: SDA and SCL ### I2C example ```javascript -var i2c = require('i2c-bus'), - i2cBus = i2c.openSync(1), - oled = require('oled-i2c-bus'); +var i2c = require('i2c-bus'); +var oled = require('oled-i2c-bus'); var opts = { width: 128, height: 64, - address: 0x3D + address: 0x3D, + bus: 1, + driver:"SSD1306" }; +var i2cbus = i2c.openSync(opts.bus) var oled = new oled(i2cBus, opts); // do cool oled things here @@ -276,6 +293,8 @@ oled.setCursor(1, 1); oled.writeString(font, 1, 'Cats and dogs are really cool animals, you know.', 1, true); ``` +Checkout https://www.npmjs.com/package/oled-font-pack for all-in-one font package. + ### update Sends the entire buffer in its current state to the oled display, effectively syncing the two. This method generally does not need to be called, unless you're messing around with the framebuffer manually before you're ready to sync with the display. It's also needed if you're choosing not to draw on the screen immediately with the built in methods. @@ -283,3 +302,67 @@ Usage: ```javascript oled.update(); ``` + +### battery +Draw a battery level in percentage indicator. This method allows for up to 4 different states of the battery: +- 0 bar : battery < 10% +- 1 bar : 10% >= battery < 40% +- 2 bar : 40% >= battery < 70% +- 3 bar : battery >= 70% + +Arguments: +* int **x** - start column +* int **y** - start row +* int **percentage** - battery level percentage + +usage: +```javascript +// args: (x,y,percentage) +oled.battery(1,1,20); +``` + +### bluetooth +Draw a bluetooth icon + +usage: +```javascript +//args: (x,y) +oled.bluetooth(1,1); +``` + +### wifi +Draw a WiFi signal strength in percentage indicator. This method allows for up to 4 different signal strength of the WiFi signal: +- 0 bar : signal < 10% +- 1 bar : 10% >= signal < 40% +- 2 bar : 40% >= signal < 70% +- 3 bar : signal >= 70% + +Arguments: +* int **x** - start column +* int **y** - start row +* int **percentage** - signal strength in percentage + +usage: +```javascript +// args: (x,y,percentage) +oled.wifi(1,1,20); +``` + +### image +A wrapper for `drawRGBAImage` that supports a fix animation. The animation always start from `x=1` and `y=1`. + +Arguments: +* int **x** - start column (ignored on `animation = true`) +* int **y** - start row (ignored on `animation=true`) +* string **image** - full path to the image or the filename of the image in the `resources` folder +* object **font** - font to draw "error" message +* boolean **clear** - clear the display before the draw +* boolean **reset** - stop all animations +* boolean **animated** - enable/disable animation +* boolean **wrapping** - enable/disable of the error message wrapping + +usage: +```javascript +var font = require('oled-font-pack') +oled.image(1,1,'rpi-frambuesa.png',font.oled_5x7,true,false,false,true); +``` \ No newline at end of file diff --git a/drivers/sh1106.js b/drivers/sh1106.js new file mode 100644 index 0000000..4969b64 --- /dev/null +++ b/drivers/sh1106.js @@ -0,0 +1,659 @@ + +var SH1106 = function (i2c, opts) { + this.HEIGHT = opts.height || 64; + this.WIDTH = opts.width || 128; + this.ADDRESS = opts.address || 0x3C; + + this.MAX_PAGE_COUNT = this.HEIGHT / 8; + this.LINESPACING = typeof opts.linespacing !== 'undefined' ? opts.linespacing : 1; + this.LETTERSPACING = typeof opts.letterspacing !== 'undefined' ? opts.letterspacing : 1; + + var config = { + '128x32': { + 'multiplex': 0x1F, + 'compins': 0x02, + 'coloffset': 0x02 + }, + '128x64': { + 'multiplex': 0x3F, + 'compins': 0x12, + 'coloffset': 0x02 + }, + '96x16': { + 'multiplex': 0x0F, + 'compins': 0x02, + 'coloffset': 0x02, + } + }; + + var screenSize = this.WIDTH + 'x' + this.HEIGHT; + this.screenConfig = config[screenSize]; + + // 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 = 0x40; + this.CHARGE_PUMP = 0xAD; + this.EXTERNAL_VCC = false; + this.MEMORY_MODE = 0x20; + this.SEG_REMAP = 0xA1; + 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_LOW_START_ADDR = 0x02; + this.COLUMN_HIGH_START_ADDR = 0x10; + this.PAGE_ADDR = 0xB0; + this.INVERT_DISPLAY = 0xA7; + this.SET_CONTRAST_CTRL_MODE = 0x81; + + this.cursor_x = 0; + this.cursor_y = 0; + + // new blank buffer (1 byte per pixel) + //For version <6.0.0 + if (typeof Buffer.alloc == "undefined") { + this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); + } + //For version >=6.0.0 + else { + this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); + } + this.buffer.fill(0xFF); + this.dirtyBytes = []; + + this.wire = i2c; + this._initialise(); +} + +/* ################################################################################################## + * OLED controls + * ################################################################################################## + */ +// turn oled on +SH1106.prototype.turnOnDisplay = function () { + this._transfer('cmd', this.DISPLAY_ON); +} + +// turn oled off +SH1106.prototype.turnOffDisplay = function () { + this._transfer('cmd', this.DISPLAY_OFF); +} + +// send dim display command to oled +SH1106.prototype.dimDisplay = function (bool) { + var contrast; + + if (bool) { + contrast = 0; // Dimmed display + } else { + contrast = 0xFF; // Bright display + } + + this._transfer('cmd', this.SET_CONTRAST_CTRL_MODE); + this._transfer('cmd', contrast); +} + +// invert pixels on oled +SH1106.prototype.invertDisplay = function (bool) { + if (bool) { + this._transfer('cmd', this.INVERT_DISPLAY); // inverted + } else { + this._transfer('cmd', this.NORMAL_DISPLAY); // non inverted + } +} + +// activate scrolling for rows start through stop +SH1106.prototype.startScroll = function (dir, start, stop) { + console.log("SH1106 do not support this command"); +} + +// stop scrolling display contents +SH1106.prototype.stopScroll = function () { + console.log("SH1106 do not support this command"); +} + +// send the entire framebuffer to the oled +SH1106.prototype.update = function () { + // wait for oled to be ready + this._waitUntilReady(function () { + // set the start and endbyte locations for oled display update + for (var pageIdx = 0; pageIdx <= this.MAX_PAGE_COUNT; pageIdx++) { + const displaySeq = [ + this.PAGE_ADDR + pageIdx, + this.COLUMN_LOW_START_ADDR, + this.COLUMN_HIGH_START_ADDR, + ]; + // send intro seq + for (var i = 0; i < displaySeq.length; i += 1) { + this._transfer('cmd', displaySeq[i]); + } + const start = pageIdx * this.WIDTH; + const end = start + this.WIDTH; + + //For version <6.0.0 + if (typeof this.buffer.subarray == "undefined") { + var pagedBuffer = this.buffer.slice(start, end) + } + //For version >=6.0.0 + else { + var pagedBuffer = this.buffer.subarray(start, end) + } + for (var i = 0; i < pagedBuffer.length; i++) { + this._transfer('data', pagedBuffer[i]); + } + } + }.bind(this)); +} + +/* ################################################################################################## + * OLED drawings + * ################################################################################################## + */ + +// clear all pixels currently on the display +SH1106.prototype.clearDisplay = function (sync) { + for (let i = 0; i < this.buffer.length; i += 1) { + if (this.buffer[i] !== 0x00) { + this.buffer[i] = 0x00; + if (this.dirtyBytes.indexOf(i) === -1) { + this.dirtyBytes.push(i); + } + } + } + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +// set starting position of a text string on the oled +SH1106.prototype.setCursor = function (x, y) { + this.cursor_x = x; + this.cursor_y = y; +} + +// buffer/ram test +SH1106.prototype.drawPageCol = function (page, col, byte) { + // wait for oled to be ready + this._waitUntilReady(function () { + // set the start and endbyte locations for oled display update + var bufferIndex = col + (page * this.WIDTH); + this.buffer[bufferIndex] = byte; + + // Ensure that column is only 0..127. + col &= 0x7F; + col += this.screenConfig.coloffset; // Column Bias for a SH1106. + + var lowAddress = (col & 0x0F); + var highAddress = this.COLUMN_HIGH_START_ADDR | (col >>> 4); + var displaySeq = [ + this.PAGE_ADDR + page, + lowAddress, + highAddress + ]; + + for (var v = 0; v < displaySeq.length; v += 1) { + this._transfer('cmd', displaySeq[v]); + } + this._transfer('data', this.buffer[bufferIndex]); + + }.bind(this)); +} + +SH1106.prototype.drawPageSeg = function (page, seg, byte, sync) { + if (page < 0 || page >= this.MAX_PAGE_COUNT || seg < 0 || seg >= this.WIDTH) { + return + } + // wait for oled to be ready + this._waitUntilReady(function () { + // set the start and endbyte locations for oled display update + var bufferIndex = seg + (page * this.WIDTH); + // console.log(`drawPageSeg -> page:${page}, seg:${seg}, index:${bufferIndex}, byte:${byte.toString(2)}`); + + this.buffer[bufferIndex] = byte; + if (this.dirtyBytes.indexOf(bufferIndex) === -1) { + this.dirtyBytes.push(bufferIndex); + } + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } + }.bind(this)); +} + +// draw one or many pixels on oled +SH1106.prototype.drawPixel = function (pixels, 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 + const x = el[0]; + const y = el[1]; + const color = el[2]; + + if (x < 0 || x >= this.WIDTH || y < 0 || y >= this.HEIGHT) { + return; + } + + // thanks, Martin Richards. + // I wanna can this, this tool is for devs who get 0 indexes + // x -= 1; y -=1; + let byte = 0; + const page = Math.floor(y / 8); + const pageShift = 0x01 << (y - 8 * page); + + // is the pixel on the first row of the page? + if (page === 0) { + byte = x; + } else { + byte = x + (this.WIDTH * page); + } + + // colors! Well, monochrome. + if (color === 'BLACK' || !color) { + this.buffer[byte] &= ~pageShift; + } else if (color === 'WHITE' || color) { + this.buffer[byte] |= pageShift; + } + + // push byte to dirty if not already there + if (this.dirtyBytes.indexOf(byte) === -1) { + this.dirtyBytes.push(byte); + } + }, this); + + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +// using Bresenham's line algorithm +SH1106.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 +SH1106.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); + } +} + +// write text to the oled +SH1106.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 RGBA image at the specified coordinates +SH1106.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 +SH1106.prototype.drawBitmap = function (pixels, sync) { + let x; + let y; + + for (let 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 (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +/* ################################################################################################## + * Private utilities + * ################################################################################################## + */ + +SH1106.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, + this.SET_START_LINE, + this.CHARGE_PUMP, 0x8B, // charge pump val + 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, 0x80, // contrast val + this.SET_PRECHARGE, 0x22, // precharge val + this.SET_VCOM_DETECT, 0x35, // vcom detect + this.NORMAL_DISPLAY, + this.DISPLAY_ON + ]; + + // write init seq commands + for (var i = 0; i < initSeq.length; i++) { + this._transfer('cmd', initSeq[i]); + } +} + +// writes both commands and data buffers to this device +SH1106.prototype._transfer = function (type, val, fn) { + var control; + if (type === 'data') { + control = 0x40; + } else if (type === 'cmd') { + control = 0x00; + } else { + return; + } + + var bufferForSend; + //For version <6.0.0 + if (typeof Buffer.from == "undefined") { + bufferForSend = new Buffer([control, val]); + } + //For version >=6.0.0 + else { + bufferForSend = Buffer.from([control, val]) + } + + // send control and actual val + this.wire.i2cWriteSync(this.ADDRESS, 2, bufferForSend); + if (fn) { + fn(); + } +} + +// read a byte from the oled +SH1106.prototype._readI2C = function (fn) { + //For version <6.0.0 + if (typeof Buffer.from == "undefined") { + this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function (_err, _bytesRead, data) { + // result is single byte + if (typeof data === "object") { + fn(data[0]); + } + else { + fn(0); + } + }); + } + //For version >=6.0.0 + else { + var data = [0]; + this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); + fn(data[0]); + } +} + +// draw an individual character to the screen +SH1106.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 +SH1106.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 +SH1106.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; +} + +// looks at dirty bytes, and sends the updated bytes to the display +SH1106.prototype._updateDirtyBytes = function (dirtyByteArray) { + var dirtyByteArrayLen = dirtyByteArray.length + + // check to see if this will even save time + if (dirtyByteArrayLen > (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 < dirtyByteArrayLen; i += 1) { + + var dirtyByteIndex = dirtyByteArray[i]; + var page = Math.floor(dirtyByteIndex / this.WIDTH); + var col = Math.floor(dirtyByteIndex % this.WIDTH); + + // Ensure that column is only 0..127. + col &= 0x7F; + col += this.screenConfig.coloffset; // Column Bias for a SH1106 + + // Compute the lower and high column addresses + var lowAddress = (col & 0x0F); // lower address ranges from 0 to 0x0F + var highAddress = this.COLUMN_HIGH_START_ADDR | (col >>> 4); // high address ranges from 0x10 to 0x18 + var displaySeq = [ + this.PAGE_ADDR + page, + lowAddress, + highAddress + ]; + + for (var v = 0; v < displaySeq.length; v += 1) { + this._transfer('cmd', displaySeq[v]); + } + this._transfer('data', this.buffer[dirtyByteIndex]); + } + // now that all bytes are synced, reset dirty state + this.dirtyBytes = []; + }.bind(this)); + } +} + +// sometimes the oled gets a bit busy with lots of bytes. +// Read the response byte to see if this is the case +SH1106.prototype._waitUntilReady = function (callback) { + var 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); +} + + +module.exports = SH1106; diff --git a/drivers/ssd1306.js b/drivers/ssd1306.js new file mode 100644 index 0000000..9093e2f --- /dev/null +++ b/drivers/ssd1306.js @@ -0,0 +1,663 @@ + +var SSD1306 = function (i2c, opts) { + this.HEIGHT = opts.height || 64; + this.WIDTH = opts.width || 128; + this.ADDRESS = opts.address || 0x3C; + + this.MAX_PAGE_COUNT = this.HEIGHT / 8; + 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; + 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 = 0x02; + this.SET_CONTRAST_CTRL_MODE = 0x81; + + this.cursor_x = 0; + this.cursor_y = 0; + + // new blank buffer (1 byte per pixel) + //For version <6.0.0 + if (typeof Buffer.alloc == "undefined") { + this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); + } + //For version >=6.0.0 + else { + this.buffer = Buffer.alloc((this.WIDTH * this.HEIGHT) / 8); + } + this.buffer.fill(0xFF); + this.dirtyBytes = []; + + var config = { + '128x32': { + 'multiplex': 0x1F, + 'compins': 0x02, + 'coloffset': 0 + }, + '128x64': { + 'multiplex': 0x3F, + 'compins': 0x12, + 'coloffset': 0 + }, + '96x16': { + 'multiplex': 0x0F, + 'compins': 0x02, + 'coloffset': 0, + } + }; + + this.wire = i2c; + var screenSize = this.WIDTH + 'x' + this.HEIGHT; + this.screenConfig = config[screenSize]; + this._initialise(); +} + +/* ################################################################################################## + * OLED controls + * ################################################################################################## + */ +// turn oled on +SSD1306.prototype.turnOnDisplay = function () { + this._transfer('cmd', this.DISPLAY_ON); +} + +// turn oled off +SSD1306.prototype.turnOffDisplay = function () { + this._transfer('cmd', this.DISPLAY_OFF); +} + +// send dim display command to oled +SSD1306.prototype.dimDisplay = function (bool) { + var contrast; + + if (bool) { + contrast = 0; // Dimmed display + } else { + contrast = 0xFF; // Bright display + } + + this._transfer('cmd', this.SET_CONTRAST_CTRL_MODE); + this._transfer('cmd', contrast); +} + +// invert pixels on oled +SSD1306.prototype.invertDisplay = function (bool) { + if (bool) { + this._transfer('cmd', this.INVERT_DISPLAY); // inverted + } else { + this._transfer('cmd', this.NORMAL_DISPLAY); // non inverted + } +} + +// activate scrolling for rows start through stop +SSD1306.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 +SSD1306.prototype.stopScroll = function () { + this._transfer('cmd', this.DEACTIVATE_SCROLL); // stahp +} + +// send the entire framebuffer to the oled +SSD1306.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 + // 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.wire.i2cWriteSync(this.ADDRESS, bufferToSend.length, bufferToSend); + }.bind(this)); +} + +/* ################################################################################################## + * OLED drawings + * ################################################################################################## + */ + +// clear all pixels currently on the display +SSD1306.prototype.clearDisplay = function (sync) { + for (let i = 0; i < this.buffer.length; i += 1) { + if (this.buffer[i] !== 0x00) { + this.buffer[i] = 0x00; + if (this.dirtyBytes.indexOf(i) === -1) { + this.dirtyBytes.push(i); + } + } + } + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +// set starting position of a text string on the oled +SSD1306.prototype.setCursor = function (x, y) { + this.cursor_x = x; + this.cursor_y = y; +} + +// buffer/ram test +SSD1306.prototype.drawPageSeg = function (page, seg, byte, sync) { + if (page < 0 || page >= this.MAX_PAGE_COUNT || seg < 0 || seg >= this.WIDTH) { + return + } + // wait for oled to be ready + this._waitUntilReady(function () { + // set the start and endbyte locations for oled display update + var bufferIndex = seg + (page * this.WIDTH); + // console.log(`drawPageSeg -> page:${page}, seg:${seg}, index:${bufferIndex}, byte:${byte.toString(2)}`); + + this.buffer[bufferIndex] = byte; + if (this.dirtyBytes.indexOf(bufferIndex) === -1) { + this.dirtyBytes.push(bufferIndex); + } + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } + }.bind(this)); +} + +// draw one or many pixels on oled +SSD1306.prototype.drawPixel = function (pixels, 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 + const x = el[0]; + const y = el[1]; + const color = el[2]; + + if (x < 0 || x >= this.WIDTH || y < 0 || y >= this.HEIGHT) { + return; + } + + // thanks, Martin Richards. + // I wanna can this, this tool is for devs who get 0 indexes + // x -= 1; y -=1; + let byte = 0; + const page = Math.floor(y / 8); + const pageShift = 0x01 << (y - 8 * page); + + // is the pixel on the first row of the page? + if (page === 0) { + byte = x; + } else { + byte = x + (this.WIDTH * page); + } + + // colors! Well, monochrome. + if (color === 'BLACK' || !color) { + this.buffer[byte] &= ~pageShift; + } else if (color === 'WHITE' || color) { + this.buffer[byte] |= pageShift; + } + + // push byte to dirty if not already there + if (this.dirtyBytes.indexOf(byte) === -1) { + this.dirtyBytes.push(byte); + } + }, this); + + if (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +// using Bresenham's line algorithm +SSD1306.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 +SSD1306.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); + } +} + +// write text to the oled +SSD1306.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 RGBA image at the specified coordinates +SSD1306.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 +SSD1306.prototype.drawBitmap = function (pixels, sync) { + let x; + let y; + + for (let 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 (sync) { + this._updateDirtyBytes(this.dirtyBytes); + } +} + +/* ################################################################################################## + * Private utilities + * ################################################################################################## + */ + +SSD1306.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 + ]; + + // write init seq commands + for (var i = 0; i < initSeq.length; i++) { + this._transfer('cmd', initSeq[i]); + } +} + +// writes both commands and data buffers to this device +SSD1306.prototype._transfer = function (type, val, fn) { + var control; + if (type === 'data') { + control = 0x40; + } else if (type === 'cmd') { + control = 0x00; + } else { + return; + } + + var bufferForSend; + //For version <6.0.0 + if (typeof Buffer.from == "undefined") { + bufferForSend = new Buffer([control, val]); + } + //For version >=6.0.0 + else { + bufferForSend = Buffer.from([control, val]) + } + + // send control and actual val + this.wire.i2cWriteSync(this.ADDRESS, 2, bufferForSend); + if (fn) { + fn(); + } +} + +// read a byte from the oled +SSD1306.prototype._readI2C = function (fn) { + //For version <6.0.0 + if (typeof Buffer.from == "undefined") { + this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function (_err, _bytesRead, data) { + // result is single byte + if (typeof data === "object") { + fn(data[0]); + } + else { + fn(0); + } + }); + } + //For version >=6.0.0 + else { + var data = [0]; + this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); + fn(data[0]); + } +} + +// sometimes the oled gets a bit busy with lots of bytes. +// Read the response byte to see if this is the case +SSD1306.prototype._waitUntilReady = function (callback) { + var 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); +} + +// draw an individual character to the screen +SSD1306.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 +SSD1306.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 +SSD1306.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; +} + +// looks at dirty bytes, and sends the updated bytes to the display +SSD1306.prototype._updateDirtyBytes = function (dirtyByteArray) { + var dirtyByteArrayLen = dirtyByteArray.length + + // check to see if this will even save time + if (dirtyByteArrayLen > (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 < dirtyByteArrayLen; i += 1) { + + var dirtyByteIndex = dirtyByteArray[i]; + var page = Math.floor(dirtyByteIndex / this.WIDTH); + var col = Math.floor(dirtyByteIndex % this.WIDTH); + + var displaySeq = [ + this.COLUMN_ADDR, col, col, // column start and end address + this.PAGE_ADDR, page, page // page start and end address + ]; + + // send intro seq + for (var v = 0; v < displaySeq.length; v += 1) { + this._transfer('cmd', displaySeq[v]); + } + // send byte, then move on to next byte + this._transfer('data', this.buffer[dirtyByteIndex]); + this.buffer[dirtyByteIndex]; + } + // now that all bytes are synced, reset dirty state + this.dirtyBytes = []; + }.bind(this)); + } +} + +module.exports = SSD1306; diff --git a/examples/README.md b/examples/README.md index 00d8e18..adcbb5d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,12 +4,20 @@ OLED JS Pi over i2c-bus ## What is this directory? -This directory contains a working example of NodeJS, a (typically) ARM board (Orange Pi Zero), and a 128x64 I2C SSD1306 display. +This directory contains a working example of NodeJS, a (typically) ARM board (Orange Pi Zero), and a 128x64 I2C SSD1306 and SH1106 display. + +Raspberry Pi allows for software I2C. To enable software I2C, add `dtoverlay=i2c-gpio,bus=3` to `/boot.config.txt`. The software I2C would be available on `bus` no `3` +where the `SDA` is on pin `GPIO23`/`BCM 16` and `SCK` is on pun `GPIO24`/`BCM 18`. In this examples, the `SSD1306` is using the hardware I2C on bus `1` while the `SH1106` +is using software I2C on bus `3`. ## Install ``` +git clone +cd /git/directory +npm install +cd examples npm install -cp ../oled.js node_modules/oled-i2c-bus/ -node clock.js +node ./ssd1306_clock.js +node ./sh1106_clock.js ``` diff --git a/examples/clock.js b/examples/clock.js deleted file mode 100644 index 4007767..0000000 --- a/examples/clock.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * clock.js - * Display a digital clock on a small I2C connected display - * - * 2016-11-28 v1.0 Harald Kubota - */ - - -"use strict"; - -// NOTE: On newer versions of Raspberry Pi the I2C is set to 1, -// however on other platforms you may need to adjust if to -// another value, for example 0. -var bus = 1; - -var i2c = require('i2c-bus'), - i2cBus = i2c.openSync(bus), - oled = require('oled-i2c-bus'); - -const SIZE_X=128, - SIZE_Y=64; - -var opts = { - width: SIZE_X, - height: SIZE_Y, - address: 0x3C -}; - - -try { - var oled = new oled(i2cBus, opts); - - oled.clearDisplay(); - oled.turnOnDisplay(); - - oled.drawPixel([ - [SIZE_X-1, 0, 1], - [SIZE_X-1, SIZE_Y-1, 1], - [0, SIZE_Y-1, 1], - [0, 0, 1] - ]); - - oled.drawLine(1, 1, SIZE_X-2, 1, 1); - oled.drawLine(SIZE_X-2, 1, SIZE_X-2, SIZE_Y-2, 1); - oled.drawLine(SIZE_X-2, SIZE_Y-2, 1, SIZE_Y-2, 1); - oled.drawLine(1, SIZE_Y-2, 1, 1, 1); -} -catch(err) { - // Print an error message and terminate the application - console.log(err.message); - process.exit(1); -} - -var font = require('oled-font-5x7'); - -// Clock - -function displayClock() { - var date=new Date(); - var hour = date.getHours(); - hour = (hour < 10 ? "0" : "") + hour; - var min = date.getMinutes(); - min = (min < 10 ? "0" : "") + min; - var sec = date.getSeconds(); - sec = (sec < 10 ? "0" : "") + sec; - - var year = date.getFullYear(); - var month = date.getMonth() + 1; - month = (month < 10 ? "0" : "") + month; - var day = date.getDate(); - day = (day < 10 ? "0" : "") + day; - - oled.setCursor(12, Math.floor(SIZE_Y/2) + 7); - oled.writeString(font, 2, hour+":"+min+":"+sec, 1, true); -} - -setInterval(displayClock, 1000); - diff --git a/examples/package-lock.json b/examples/package-lock.json new file mode 100644 index 0000000..4e50139 --- /dev/null +++ b/examples/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "examples", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "examples", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "oled-font-pack": "^1.0.1", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "node_modules/i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/oled-font-pack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/oled-font-pack/-/oled-font-pack-1.0.1.tgz", + "integrity": "sha512-vu10Fx63RJfKXcuYdgd9/6T0+u8D4GW3NP4yTJiI4pe/GV384p+O3sLxfpEYIszZjrtBxEZh/u8xXU7S9FMH6w==" + }, + "node_modules/png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "dependencies": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "node_modules/temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "dependencies": { + "es6-shim": "latest" + }, + "engines": { + "node": ">=0.8.0" + } + } + }, + "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "requires": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + } + }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "oled-font-pack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/oled-font-pack/-/oled-font-pack-1.0.1.tgz", + "integrity": "sha512-vu10Fx63RJfKXcuYdgd9/6T0+u8D4GW3NP4yTJiI4pe/GV384p+O3sLxfpEYIszZjrtBxEZh/u8xXU7S9FMH6w==" + }, + "png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "requires": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, + "pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "requires": { + "es6-shim": "latest" + } + } + } +} diff --git a/examples/package.json b/examples/package.json index 2baf2a0..8acee95 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,7 +1,7 @@ { "name": "examples", "version": "1.0.0", - "description": "Testing OLED SSD1306 display with nodeJS", + "description": "Testing OLED SSD1306 and SH1106 display with nodeJS", "main": "clock.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -17,9 +17,11 @@ "author": "Harald Kubota", "license": "MIT", "dependencies": { - "i2c-bus": "^1.1.2", - "oled-font-5x7": "^1.0.0", - "oled-i2c-bus": "^1.0.11", - "pngjs": "^3.3.3" + "i2c-bus": "^5.2.2", + "oled-font-pack": "^1.0.1", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" } } diff --git a/examples/sh1106_clock.js b/examples/sh1106_clock.js new file mode 100644 index 0000000..4866b9e --- /dev/null +++ b/examples/sh1106_clock.js @@ -0,0 +1,68 @@ + +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../oled'); +const font = require('oled-font-pack'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +var battery = 0; +var signal = 0; +var _oled; +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + _oled = oled; + oled.clearDisplay(true); + + + oled.drawLine(0,0,WIDTH-1,0,1,false); + oled.drawLine(0,HEIGHT-1,WIDTH-1,HEIGHT-1,1,false); + oled.drawLine(0,0,0,HEIGHT-1,1,false); + oled.drawLine(WIDTH-1,0,WIDTH-1,HEIGHT-1,1,false); + oled.bluetooth(25,2); + + oled.image(54,3,"OledImage.png",font.oled_5x7,false,false,false,false); + setInterval(displayClock, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err); + process.exit(1); +} + + +function displayClock() { + var date = new Date(); + var hour = date.getHours(); + hour = (hour < 10 ? "0" : "") + hour; + var min = date.getMinutes(); + min = (min < 10 ? "0" : "") + min; + var sec = date.getSeconds(); + sec = (sec < 10 ? "0" : "") + sec; + + var month = date.getMonth() + 1; + month = (month < 10 ? "0" : "") + month; + var day = date.getDate(); + day = (day < 10 ? "0" : "") + day; + + oled.setCursor(20, Math.floor(HEIGHT / 2) +5); + oled.writeString(font.oled_5x7, 2, hour + ":" + min + ":" + sec, 1, true); + + battery = (battery+20)%100; + signal = (signal+20)%100; + oled.battery(5,4,battery); + oled.wifi(105,2,signal); +} \ No newline at end of file diff --git a/examples/rgba.js b/examples/sh1106_rgba.js similarity index 90% rename from examples/rgba.js rename to examples/sh1106_rgba.js index 10f0b1d..a2777d4 100644 --- a/examples/rgba.js +++ b/examples/sh1106_rgba.js @@ -13,16 +13,17 @@ const PNG = require('pngjs').PNG; const i2c = require('i2c-bus'); const oled = require('../oled');// 'oled-i2c-bus'); -var i2cBus = i2c.openSync(0); + var opts = { width: 128, height: 64, - address: 0x3C + address: 0x3C, + bus:3, + driver: 'SH1106' }; - +var i2cBus = i2c.openSync(opts.bus); var display = new oled(i2cBus, opts); - display.clearDisplay(); display.turnOnDisplay(); diff --git a/examples/ssd1306_clock.js b/examples/ssd1306_clock.js new file mode 100644 index 0000000..271fabd --- /dev/null +++ b/examples/ssd1306_clock.js @@ -0,0 +1,68 @@ + +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../oled'); +const font = require('oled-font-pack'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 1, + driver: 'SSD1306' +}; + +var battery = 0; +var signal = 0; +var _oled; +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + _oled = oled; + oled.clearDisplay(true); + + + oled.drawLine(0,0,WIDTH-1,0,1,false); + oled.drawLine(0,HEIGHT-1,WIDTH-1,HEIGHT-1,1,false); + oled.drawLine(0,0,0,HEIGHT-1,1,false); + oled.drawLine(WIDTH-1,0,WIDTH-1,HEIGHT-1,1,false); + oled.bluetooth(25,2); + + oled.image(54,3,"OledImage.png",font.oled_5x7,false,false,false,false); + setInterval(displayClock, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err); + process.exit(1); +} + + +function displayClock() { + var date = new Date(); + var hour = date.getHours(); + hour = (hour < 10 ? "0" : "") + hour; + var min = date.getMinutes(); + min = (min < 10 ? "0" : "") + min; + var sec = date.getSeconds(); + sec = (sec < 10 ? "0" : "") + sec; + + var month = date.getMonth() + 1; + month = (month < 10 ? "0" : "") + month; + var day = date.getDate(); + day = (day < 10 ? "0" : "") + day; + + oled.setCursor(20, Math.floor(HEIGHT / 2) +5); + oled.writeString(font.oled_5x7, 2, hour + ":" + min + ":" + sec, 1, true); + + battery = (battery+20)%100; + signal = (signal+20)%100; + oled.battery(5,4,battery); + oled.wifi(105,2,signal); +} \ No newline at end of file diff --git a/examples/ssd1306_rgba.js b/examples/ssd1306_rgba.js new file mode 100644 index 0000000..3fbfee5 --- /dev/null +++ b/examples/ssd1306_rgba.js @@ -0,0 +1,39 @@ +/* + * rgba.js + * Display an RGBA image at random locations on a small I2C connected display + * + * 2018-08-19 v1.0 Bryan Nielsen + */ + + +"use strict"; + +const fs = require('fs'); +const PNG = require('pngjs').PNG; +const i2c = require('i2c-bus'); +const oled = require('../oled');// 'oled-i2c-bus'); + +var opts = { + width: 128, + height: 64, + address: 0x3C, + bus:1, + driver: 'SSD1306' +}; + +var i2cBus = i2c.openSync(opts.bus); +var display = new oled(i2cBus, opts); +display.clearDisplay(); +display.turnOnDisplay(); + +fs.createReadStream('./test.png') +.pipe(new PNG({ filterType: 4 })) +.on('parsed', function () { + setInterval(() => { drawImage(this) }, 1000); +}); + +function drawImage(image) { + let x = Math.floor(Math.random() * (display.WIDTH) - image.width / 2); + let y = Math.floor(Math.random() * (display.HEIGHT) - image.height / 2); + display.drawRGBAImage(image, x, y); +} diff --git a/oled.js b/oled.js index d4a6938..329302d 100644 --- a/oled.js +++ b/oled.js @@ -1,638 +1,297 @@ -var Oled = function(i2c, opts) { - - this.HEIGHT = opts.height || 32; - this.WIDTH = opts.width || 128; - this.ADDRESS = opts.address || 0x3C; - 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 - //For version <6.0.0 - if(typeof Buffer.alloc == "undefined") { - this.buffer = new Buffer((this.WIDTH * this.HEIGHT) / 8); - } - //For version >=6.0.0 - else { - 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, +const SSD1306 = require('./drivers/ssd1306'); +const SH1106 = require('./drivers/sh1106'); +const fs = require('fs'); +const PNG = require('pngjs').PNG; + +var pdxb = null; +var pdyb = null; +var timers = []; + +var Oled = function (i2c, opts) { + this.DRIVER = opts.driver || "SSD1306"; + this.HEIGHT = opts.height || 64; + this.WIDTH = opts.width || 128; + + switch (this.DRIVER) { + case "SSD1306": + this.api = new SSD1306(i2c, opts); + break; + case "SH1106": + this.api = new SH1106(i2c, opts); + break; + default: + throw new Error("Unknown Driver"); } - }; - // Setup i2c - //console.log('this.ADDRESS: ' + this.ADDRESS); - this.wire = i2c; - - 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]); - } +/* ###################################################################### + * OLED Controls + * ###################################################################### + */ +// turn oled on +Oled.prototype.turnOnDisplay = function () { + this.api.turnOnDisplay(); } -// 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, sentCount; - //For version <6.0.0 - if(typeof Buffer.from == "undefined") { - bufferForSend = new Buffer([control, val]); - } - //For version >=6.0.0 - else { - bufferForSend = Buffer.from([control, val]) - } - - // send control and actual val - sentCount = this.wire.i2cWriteSync(this.ADDRESS, 2, bufferForSend); - if(fn) { - fn(); - } +// turn oled off +Oled.prototype.turnOffDisplay = function () { + this.api.turnOffDisplay(); } -// read a byte from the oled -Oled.prototype._readI2C = function(fn) { - //For version <6.0.0 - if(typeof Buffer.from == "undefined") { - this.wire.i2cRead(this.ADDRESS, 0, new Buffer([0]), function(err, bytesRead, data) { - // result is single byte - if(typeof data === "object") { - fn(data[0]); - } - else { - fn(0); - } - }); - } - //For version >=6.0.0 - else { - var data=[0]; - this.wire.i2cReadSync(this.ADDRESS, 1, Buffer.from(data)); - fn(data[0]); - } +// send dim display command to oled +Oled.prototype.dimDisplay = function (bool) { + this.api.dimDisplay(bool); } -// 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); +// invert pixels on oled +Oled.prototype.invertDisplay = function (bool) { + this.api.invertDisplay(bool); } -// set starting position of a text string on the oled -Oled.prototype.setCursor = function(x, y) { - this.cursor_x = x; - this.cursor_y = y; +// activate scrolling for rows start through stop +Oled.prototype.startScroll = function (dir, start, stop) { + this.api.startScroll(dir, start, stop); } -// 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); - } +// stop scrolling display contents +Oled.prototype.stopScroll = function () { + this.api.stopScroll(); } -// 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); - } - } - } +// send the entire framebuffer to the oled +Oled.prototype.update = function () { + // wait for oled to be ready + this.api.update(); } -// 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; -} +/* ###################################################################### + * OLED Drawings + * ###################################################################### + */ -// 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; +// clear all pixels currently on the display +Oled.prototype.clearDisplay = function (sync) { + this.api.clearDisplay(sync); } -// 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]); - var sentCount = this.wire.i2cWriteSync(this.ADDRESS, bufferToSend.length, bufferToSend); - - }.bind(this)); +// set starting position of a text string on the oled +Oled.prototype.setCursor = function (x, y) { + this.api.setCursor(x, y); } -// 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); +Oled.prototype.drawPageCol = function (page, col, byte) { + this.api.drawPageCol(page, col, byte); } -// turn oled off -Oled.prototype.turnOffDisplay = function() { - this._transfer('cmd', this.DISPLAY_OFF); +// buffer/ram test +Oled.prototype.drawPageSeg = function (page, seg, byte, sync) { + this.api.drawPageSeg(page, seg, byte, sync); } -// turn oled on -Oled.prototype.turnOnDisplay = function() { - this._transfer('cmd', this.DISPLAY_ON); +// draw one or many pixels on oled +Oled.prototype.drawPixel = function (pixels, sync) { + this.api.drawPixel(pixels, sync); } -// 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(); - } +// using Bresenham's line algorithm +Oled.prototype.drawLine = function (x0, y0, x1, y1, color, sync) { + this.api.drawLine(x0, y0, x1, y1, color, sync); +} +// draw a filled rectangle on the oled +Oled.prototype.fillRect = function (x, y, w, h, color, sync) { + this.api.fillRect(x, y, w, h, color, sync); } -// 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 - } +// write text to the oled +Oled.prototype.writeString = function (font, size, string, color, wrap, sync) { + this.api.writeString(font, size, string, color, wrap, sync); } // 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); - } +Oled.prototype.drawRGBAImage = function (image, dx, dy, sync) { + this.api.drawRGBAImage(image, dx, dy, sync); } // 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); - } +Oled.prototype.drawBitmap = function (pixels, sync) { + this.api.drawBitmap(pixels, sync); } -// 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; +/* ###################################################################### + * OLED Shape/Indicators + * ###################################################################### + */ + +Oled.prototype.battery = function (x, y, percentage) { + this.drawLine(x, y, x + 16, y, 1) + this.drawLine(x, y + 8, x + 16, y + 8, 1) + this.drawLine(x, y, x, y + 8, 1) + this.drawPixel([[x + 17, y + 1, 1], [x + 17, y + 7, 1]]) + this.drawLine(x + 18, y + 1, x + 18, y + 7, 1) + + if (percentage >= 70) { + this.fillRect(x + 2, y + 2, 3, 5, 1, false) + this.fillRect(x + 7, y + 2, 3, 5, 1, false) + this.fillRect(x + 12, y + 2, 3, 5, 1, true) } - // push byte to dirty if not already there - if (this.dirtyBytes.indexOf(byte) === -1) { - this.dirtyBytes.push(byte); + if (percentage >= 40 && percentage < 70) { + this.fillRect(x + 2, y + 2, 3, 5, 1, false) + this.fillRect(x + 7, y + 2, 3, 5, 1, false) + this.fillRect(x + 12, y + 2, 3, 5, 0, true) } - }, this); + if (percentage >= 10 && percentage < 40) { + this.fillRect(x + 2, y + 2, 3, 5, 1, false) + this.fillRect(x + 7, y + 2, 3, 5, 0, false) + this.fillRect(x + 12, y + 2, 3, 5, 0, true) + } - if (immed) { - this._updateDirtyBytes(this.dirtyBytes); - } + if (percentage < 10) { + this.fillRect(x + 2, y + 2, 3, 5, 0, false) + this.fillRect(x + 7, y + 2, 3, 5, 0, false) + this.fillRect(x + 12, y + 2, 3, 5, 0, true) + } } -// 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 { +Oled.prototype.bluetooth = function (x, y) { + this.drawLine(x + 5, y + 1, x + 5, y + 11, 1) + this.drawLine(x + 2, y + 3, x + 9, y + 8, 1) + this.drawLine(x + 2, y + 9, x + 8, y + 3, 1) + this.drawLine(x + 5, y + 1, x + 9, y + 3, 1) + this.drawLine(x + 5, y + 11, x + 8, y + 9, 1) +} - this._waitUntilReady(function() { - // iterate through dirty bytes - for (var i = 0; i < blen; i += 1) { +Oled.prototype.wifi = function (x, y, percentage) { + this.drawLine(x, y, x + 8, y, 1) + this.drawLine(x, y, x + 4, y + 4, 1) + this.drawLine(x + 8, y, x + 4, y + 4, 1) + this.drawLine(x + 4, y, x + 4, y + 9, 1) - var byte = byteArray[i]; - var page = Math.floor(byte / this.WIDTH); - var col = Math.floor(byte % this.WIDTH); + if (percentage >= 70) { + this.fillRect(x + 6, y + 8, 2, 2, 1, true) + this.fillRect(x + 10, y + 6, 2, 4, 1, true) + this.fillRect(x + 14, y + 4, 2, 6, 1, true) + } - var displaySeq = [ - this.COLUMN_ADDR, col, col, // column start and end address - this.PAGE_ADDR, page, page // page start and end address - ]; + if (percentage >= 40 && percentage < 70) { + this.fillRect(x + 6, y + 8, 2, 2, 1, true) + this.fillRect(x + 10, y + 6, 2, 4, 1, true) + this.fillRect(x + 14, y + 4, 2, 6, 0, true) + } - var displaySeqLen = displaySeq.length, v; + if (percentage >= 10 && percentage < 40) { + this.fillRect(x + 6, y + 8, 2, 2, 1, true) + this.fillRect(x + 10, y + 6, 2, 4, 0, true) + this.fillRect(x + 14, y + 4, 2, 6, 0, true) + } - // 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 = []; + if (percentage < 10) { + this.fillRect(x + 6, y + 8, 2, 2, 0, true) + this.fillRect(x + 10, y + 6, 2, 4, 0, true) + this.fillRect(x + 14, y + 4, 2, 6, 0, true) + } } -// 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); +Oled.prototype.image = function (x, y, image, font, clear, reset, animated, wrapping) { + + var dirresources = __dirname + "/resources/"; + // console.log(dirresources) + if (typeof reset === 'boolean' && reset) { + timers.forEach(function (entry) { + clearInterval(entry); + entry = null; + }); + timers = []; + if (typeof clear === 'boolean' && clear) { + this.clearDisplay(); + } + if (typeof pdxb === 'number') { pdxb = null } + if (typeof pdyb === 'number') { pdyb = null } + return + } - if (x0 === x1 && y0 === y1) break; + if (typeof image === 'string' && !image.includes("/")) { + tryImage = image; + image = dirresources + image; + } - var e2 = err; + try { + if (!fs.statSync(image).isFile()) { + console.log("file " + image + "not exist."); + } + } catch (err) { + image = dirresources + "notafile.png"; + x = 0; + y = 17; + this.clearDisplay(); + this.writeString(font, 1, tryImage, 1, wrapping) + } - if (e2 > -dx) {err -= dy; x0 += sx;} - if (e2 < dy) {err += dx; y0 += sy;} - } + if (typeof clear === 'boolean' && clear) { + this.clearDisplay(); + } - if (immed) { - this._updateDirtyBytes(this.dirtyBytes); - } + try { + const _oled = this; + fs.createReadStream(image) + .pipe(new PNG({ filterType: 4 })) + .on('parsed', function () { + if (typeof animated === 'boolean' && animated) { + pdxb = 1; + pdyb = -1; + try { + let myInterval = setInterval(() => { _drawPseudo(_oled, clear, this, pdxb, pdyb) }, 10); + timers.push(myInterval); + } catch (e) { console.log(e) } + + } + else { + _oled.api.drawRGBAImage(this, x || Math.floor((_oled.WIDTH - this.width) / 2), y || Math.floor((_oled.HEIGHT - this.height) / 2), true); + } + }); + } catch (err) { + console.error(err) + } } -// 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); - } -} +function _drawPseudo(display, clear, image, pdxb, pdyb) { + var image; + if (typeof this.init === "undefined" || this.init === true || this.image !== image) { + this.init = false; + this.image = image; + this.x = 1; + this.y = 1; + this.prevX = 1; + this.prevY = 1; + this.dx = pdxb; + this.dy = pdyb; + //console.log("entra drawPseudo this.x " + this.x + " this.y " + this.y + " this.dx " + this.dx + " this.dy " + this.dy); + } + if (clear) { + display.fillRect(0, 0, display.WIDTH, display.HEIGHT, 1, true) + display.fillRect(1, 1, display.WIDTH - 2, display.HEIGHT - 2, 0, true) + + } else { + display.fillRect(this.prevX, this.prevY, image.width, image.height, 0, false); + this.prevX = this.x; + this.prevY = this.y; + // display.fillRect(0,0,display.WIDTH , display.HEIGHT ,1,true) + // display.fillRect(1,1,display.WIDTH - 2, display.HEIGHT - 2 ,0,true) + } -// 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]); + display.drawRGBAImage(image, this.x, this.y, true); + if (this.x + this.dx > display.WIDTH - image.width || this.x < 1) { + this.dx = -this.dx; + } + if (this.y + this.dy > display.HEIGHT - image.height || this.y < 1) { + this.dy = -this.dy; } - }.bind(this)); -} -// stop scrolling display contents -Oled.prototype.stopScroll = function() { - this._transfer('cmd', this.DEACTIVATE_SCROLL); // stahp + this.x += this.dx; + this.y += this.dy; } module.exports = Oled; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7e75a39 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,185 @@ +{ + "name": "oled-rpi-i2c-bus", + "version": "1.1.2", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "oled-rpi-i2c-bus", + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "pngjs": "^3.3.3" + }, + "devDependencies": { + "oled-font-5x7": "~1.0.0", + "png-to-lcd": "~1.0.2", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==", + "dev": true + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==", + "dev": true + }, + "node_modules/i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/oled-font-5x7": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/oled-font-5x7/-/oled-font-5x7-1.0.3.tgz", + "integrity": "sha512-l25WvKft8CgXYxtaqKdYrAS1P91rnUUUIiOXojAOvjNCsfFzIl1aEsE2JuaRgMh1Euo7slm5lX0w+1qNkL8PpQ==", + "dev": true + }, + "node_modules/png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "dev": true, + "dependencies": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==", + "dev": true + }, + "node_modules/temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "dev": true, + "dependencies": { + "es6-shim": "latest" + }, + "engines": { + "node": ">=0.8.0" + } + } + }, + "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==", + "dev": true + }, + "i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "requires": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + } + }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "oled-font-5x7": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/oled-font-5x7/-/oled-font-5x7-1.0.3.tgz", + "integrity": "sha512-l25WvKft8CgXYxtaqKdYrAS1P91rnUUUIiOXojAOvjNCsfFzIl1aEsE2JuaRgMh1Euo7slm5lX0w+1qNkL8PpQ==", + "dev": true + }, + "png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "dev": true, + "requires": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, + "pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==", + "dev": true + }, + "temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "dev": true, + "requires": { + "es6-shim": "latest" + } + } + } +} diff --git a/package.json b/package.json index e911230..77d4a10 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,11 @@ { "name": "oled-i2c-bus", - "version": "1.0.12", - "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 OLED screens)", + "version": "1.1.0", + "description": "NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 and SH1106 OLED screens)", "main": "oled.js", "devDependencies": { - "i2c-bus": "^4.0.1", - "oled-font-5x7": "~1.0.0", + "oled-font-pack": "^1.0.1", "png-to-lcd": "~1.0.2", - "pngjs": "^3.3.3", "pngparse": "~2.0.1", "temporal": "^0.3.8" }, @@ -26,10 +24,19 @@ "SSD1306", "128x64" ], - "author": ["Judd Flamm", "Suz Hinton", "Bogdan Symchych"], + "author": [ + "Judd Flamm", + "Suz Hinton", + "Bogdan Symchych", + "Abdul Hadi Fikri" + ], "license": "MIT", "bugs": { "url": "https://github.com/kd7yva/oled-i2c-bus/issues" }, - "homepage": "https://github.com/baltazorr/oled-i2c-bus" + "homepage": "https://github.com/baltazorr/oled-i2c-bus", + "dependencies": { + "i2c-bus": "^5.2.2", + "pngjs": "^3.3.3" + } } diff --git a/resources/OledBattery.png b/resources/OledBattery.png new file mode 100644 index 0000000000000000000000000000000000000000..0a4f498c4be059adedee25a51bc769fe0a7b642c GIT binary patch literal 232 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 I>zopr0K+vzIsgCw literal 0 HcmV?d00001 diff --git a/resources/OledCleardigit.png b/resources/OledCleardigit.png new file mode 100644 index 0000000000000000000000000000000000000000..abd9645df261b169c116f4522586f0fc0b9f7cdb GIT binary patch literal 4781 zcmeHKdr%YS77soU5TD>1btg!DF4=?xvMb>kgs1@oR8*+hWH+!v9wZwHsAyGG#0N?R zR74Ripv60==vCy38XiTcSVcxH$i<4{h+wa!)E2#8!b6;P=8iMB|B;VPOuw7mRm{Vlaku#D|2^L3k{r)~MuEGzrmh zY7!##l$^oPKWa-`cK$JB?ek%kPr5{8k@QT0I5l{tKN(rxntb$oZktMC9a?#?*6g7A zkFLgBF1Cy3Cg!!w&ApgyuydMZy>a1^!>}Ue((@%@+P4eF(BE3_@TKp84*TkzDbbNWUDh7uw>+)P-PR;BJwt^KISyCSc5lB{buXtBe_?;W zE2uT8s_6d0f+*Y97n!UHR~%5myY!zu-W{o0+CAyc-TGa}pD|N;%&X*P*Aka@ZB3|8 zl4ZVs)Ux`Z7K`|%@jIi&Sip)MzW7afP2Rrn@WKg?R+z<9@NI^!%=O!ArNiWF^O=T9 zU&*a4SI#fZPL64rw{5N6A#B=Y=M*yEW`k{l!Rwx)aa~5EUDh@kQnxU!hVmZ@?zl7aWCiRV>TP>HcSZ{K1@f%*&Wzpc%7RR-dZn)Y~Gfh zeR0Rh??IZ`DT{LxZlr8s zce3xw0Ojkf65{)>AeWJXYfUAIT-Ota96rl1pTHe&C`MEQ2@I;DO9A zJZhCr>Y9aBc2Bx2d6P8l!~82qZHef0tnwduMKLb*qF1ho_W4Jf-s0~&Zhl$5zd^OB z>z<*wyP~%5dNSqZKQB}qZ$JKa)5vo*RhKq07-or-w|Ah(+q|eC#lH z54&TTw&AA?A5JGuWY4hs^=U&%MZ-dm$dC;AFMG_6D37)hUezrt${JS=8|Pu+wClTS zPRp#h!|%M&jhVT&z1j8l?>`>T38-0P-H~LtP>m>>M>Xe*A2(eKu)`fzIk?TJs4T|&h^EEuU1vv0-le{c_f4K**8RA&Oy?Wi zx#jkBNym37;)jR+;3j&R>D_ZwRc1pkdqfu0t=TX$cZ|o5tBV^LBX*WX9ynhwXq^7) zb=woCw)Y8)TtsuNuCS8>hEzNShEb?^DJD}X9C1P=B^~t&H5gV5hO4_?jmx4)8j_L` zlv2QKFTT!%C_=zo$rB@DwKo|_`NeC<;CTNKS$vcXC7AARR<3#s04PWrhxCeQr54i* zm?m5dJR8Lk?V-SK6+|Bhv{YoxoQYG79?EM3jy8)%t)G6 zV=Pu|Y^-CflcP!#!D6E*%0f6S4hIGrur^Lf<9b-BwKqcaU zqXkSRScm%JQ>ew_et4y}mj%EFOOLBrY)6EpP_PDCXlbAI0HimdzqHVXfSJw;BDE@= zMn?LqCzZ7QKnOzCZ?D#AqD|=#G8P$4DuAjMcx8X$(pMx7?6)vd5J4%_CM!VpCz>=R z|47y+u^CrP=?n}6nD^s;qTQFfNf~H~#h6ef(;34P2?b1}e~eJcC;~G*@?B82oWmwy zl85qOF5gK8Tqz@sPypPK0yu2M8Ij33JXlI{2$;)7 zU0^@CcHnRz{e77zxKb14RNR#}WA`35>>RIWQ0~#kdR# z2>!SdLMg~#8aL9(cIKhZs0*9NLtI>(xy~PzR*)JkXhkC^8*y~vm{yE+!9X^ES=`vD z0Kg;%$za|Z5~o#~5S1!gz%<4L87=#l#b7@XIE@Q&ngpN-hl3$}4B>^aIT#Pc*u41& zkPOgQ5tKace`yyJt=7M^(0}6 z6JYf$$s%!O1PR7RZ@>1*slVt2LdxTDq0au_uO`v%@zqgL;Nb*s@dZhkTs)l#D}cpB>ylblzU`i jLz-oxS9ahW+xNC^>0g>H%sEw!0uzSF$6t8POS1L9&z}j1 literal 0 HcmV?d00001 diff --git a/resources/OledClock.png b/resources/OledClock.png new file mode 100644 index 0000000000000000000000000000000000000000..4f26e2ae0002f46c549e29a173e835479083b39d GIT binary patch literal 5111 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 literal 0 HcmV?d00001 diff --git a/resources/OledClockWhite.png b/resources/OledClockWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..626b5ee0e6bd9c104274e07ceab88bc540865a7f GIT binary patch literal 5137 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 A;Q#;t literal 0 HcmV?d00001 diff --git a/resources/OledDim.png b/resources/OledDim.png new file mode 100644 index 0000000000000000000000000000000000000000..ead31a2e764e8961b54660f0afc77be08f21c005 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz1!3HFCgzU0`6id3JuOkD)#(wTUiL5|AXMsm# zF#`kNJ`iSn<+SktP*AeOHKHUqKdq!Zu_%?HATcwqL@zJ3M8QPQK+nkVqeA9XprSxe z7sn6}@3&_b@-i4OFdcli-()LChX1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T2H4A(%TS))_02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{006>CL_t(I%e_=V4#OY}bNE6yRqBoZzlbM*)E{^km}nQe zg-x@>fhe&du_AZ{F^5bqDJABd5o0tfNut&&Q|gowo4R#uQusgs2mq+6P}NTf*uKSp z1(Z_ImEGtK{lD_Hb98i^F-8KQ^lk&VH-Ir#83N+4Cjm$>f+zG>sVeq;=a?@>@>F^D zEtfr;|JUq!9LH0Tjey?K{>GlQRyWC4_6*l~x{Fr|d$DZWHX7jF4(oX$3uX~>^|mqa cW(l0~AJUOlQ;3i0i2wiq07*qoM6N<$g79!T0ssI2 literal 0 HcmV?d00001 diff --git a/resources/OledEightWhite.png b/resources/OledEightWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..6669777f61dd22d4a3ff82dce79f3fa3d359e246 GIT binary patch literal 7698 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 literal 0 HcmV?d00001 diff --git a/resources/OledEightWhiteT.png b/resources/OledEightWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5b7ead11b8a113dcfbeac8bbec6cccc936bafd GIT binary patch literal 625 zcmV-%0*?KOP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T3G8Zf}*8Bhf02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002ozL_t(I%k7fE3IHGoLtX#>&+8;qtT4Q8H^FV74{?Yj z<01gq=BiNzpwnD#RQp(sGWk(7q9L0X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T55FKdHt*ih502y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002r!L_t(I%k7de4gfF+0-^u^Go3oM#;Aj7;%yGV;1M^1 z4b8&|UKv)FBa6=_dCe9J1ty$$)%i33(=&>)O&Y^G^^8g)0PNFq12dQpA-@LiVE_OC M07*qoM6N<$g1wvlDF6Tf literal 0 HcmV?d00001 diff --git a/resources/OledFiveWhite.png b/resources/OledFiveWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf9ad014f6a72922f58b304f95252cf2a339c7f GIT binary patch literal 7085 zcmeHMc{r5s*SD`xq$sK}mJ+iw%w!2OS;wATdS=078D@q-D3!8=Bo$ebBH0o}w$g^A zvZq9aC|hMomb?%3?c4W#f7kV1-|Kqc|C+g;XP)QW=X~ySKKHrr>zr$1tSpSgL^g|X zadC;67!z#3-?iYiNk{;^PdKrkaB+#A4z+V++W;JhKa=4>_n|@9A^tQ7Etu}X#TERr zD3#7s*d=~!c}j}KcVt)MeGRf7vLS12;b}F-QtxX^k%EoZiqU+MF*lr+7bdaGZ->gH z)|27+xzv!T&DXrsr4H84f0L4ZIz038t94($rp+Wmu0q@BkefZH-@SL(bz(FW0Zsgz z7_)VIn^qjdy*#LYa&q$PK?4U}3%1LfA?Kc_3xZz>eXlo0jfVDayyE<&x~L_fL~g=m zXdE#&W@CPuH>jU=}a~kx9HDD(iFWf_LoJAz0-i z_e%_tS;MsaQnURoQEdxqkG9YYMP1W-m&RN-)+g~F-kiH@o2wvqOe^UmyMBRBiz?_n zB^$l>MAb;~tLxX+9#<#n9wp6)T{nn+U@0CYbD*_h9mO2gAnEPXw7sR~4AUmmuV?ra z#R4}>X0{Z~Nt&wV589Z*imEpWV#ur+ine|TT0My=RA z&tnosn?uf68(r|5H#zc5eM+tD=`E^+scKrh;}^+-f?uV-(af$xD78NpIjvCjOiuRp?b1lKa+$+I#qx;*?@CPhM`ALfvRUB>gK8X~)p4UN8E*YJ0=48nc% z!g_c0bmjD+Y+npMZ`Wx|i*KG4_Zx1(zHgk5KY8mcWr03{%w^bymo5gyW$@Ctzas_p z?sAQ7P1!b8of;dcU>fbrxr5s|Oo-XFZ_jym$I{VH&7F1!6jU+85rDbl2Ep+&yL$0E zJ>I4OFM`Eol2w(-) zp)boe92J!f`>HC5^0t`08CagQPYnyTR(CxSD;{=bA5<`HtBY8JcQH|{qHFlDq0`ZW zI^%CW&GJ)k-z$#D>olaRu5T;*dgf|lS@K~Ur}(w6f+8ck_ZI}5GqE}?6UVAnu=TV} za5T9+jl>hvAnb#<%jS~SnQ%YJGe^33zTDouC=rN59Q0wd-4fnssdBdPrqM||WtSeT zL&0U;l5DK?&6#1zK>R`op4Ia8YkR80)OQd3R7k}k^y@G0U0>v<%DJ@`Y%tV&{^-1L z3wlAQ`>J!$2XF5xjy3G2hhk>e7fKpwlN-`uTGYb0hj)HD++&C8HRmq5a9pO^mUrxr zB2k)6##`R>7YN7%I#(V~TkO4bZOA*$$#Tzm`U5oJcjM^uBN#Iu{rwYg)$MO8#cpAL#759S|72GQg7N^4)W=D$@ObX49~{Id5<0=TqM7%me9?n>-No^ zpm@Z1Dz1A@lgLGmHbod)UTqQ#68x+onl+X*Rrbm2*nzjW#Rs$R1G=hfWxw4{j`*OK z^EqT;RMeC1iwC~>zqs#~l};Pf%$WYD7Wn<S;qzdF@e4_ zocXLldqeW!>HF)dAJ?m8BRGa}M&4j) z>HRwi?=Ctz%y>j5J`Ng)h9Yg>Z4QZ$MmLhv4U4iHZKEV@%82Uwt%+1<6{P)PJXK=0 zNKIvjpplNe5RXlGrh2W2BwA55sbGj$^+=n)WZmh9g{dxP(cew!n+h%-cN83QYkYep zN+9FabiM?=l&fR@9ZxgWE;#pspmonf`9xbc9=Y<^=qBvV-Y5&pk)_>34Hni(`XaHT zPhZ&_Z42HklmfdZEX?3bsxMSp7ptRwysnv)@FD2BBu`!>H(+e7y@P+71CA1Ru1qC7 z`c`af&-eJIlTT+E<{n#fm&U2@eX|zsn${j0-fH_T%p_l&;|BGxDU?v+4-(JlP?Fn- z_efilIwx|7_nSR&t3B`Ao*7~8tF}Qd&5CypUQIUEyi`b7Yna)3jZ}9e5&umqXM9H_ z;l9-7`J=&?Ma#K+paR%N(o#^jK;0MT%iXN<^}dJ<%pdm(;XR zHs-^B^!&}t6>*wuf!Af z43>RiUqM44ny*rz*M+hppXe!x_n&xQ{xvtIDgZmFvUEa!>T_f0spa7zVx@isBAkPe@`HVM-!M!VbJ(eqEa9X=!KlUY0;_4;Da z!bj=iOFiA@CRX)_dFYq&T+FiWKQ~gm!R?&nQKK6ihkLbAMCth1{`S67ql$SvzO^bW zpYvs$nscC>OXcIyQ}=ll9Z-1)_UD~xBGV7+F@1ryVxK>Bv=}RFvj|s~!0f$H;Qttt z>h-`|qI14*`9;jY37gA(i*(<@KBP_SpKNQ;O}y20 zk#%G|T{CcGS=COl9ntb0d8!&Fh5P zv@CP435&{Zd1f)m#l?M$ZfIy_VrckRbq?0#>ETH_#`ky2J#@FdN!%$Ov@W30Dnre2 zLqNw)sceI*8=M1?x2HwZ)D>0W2}iu^>k}VdI~rG3EM9yBGROUHWbpC`ROvfHr967( zP35-937v^PZiCt<>IL3%&)KicmEMt+t9J_QlOPD`M9V8UWi9GsgAl^$}+ zc(#O_??`{hm1V5myrx~b50cqeF7|Zc0bqY-?>p8zR5ILnu`%%BfaX*FbNrc`u~ja&7BQeRdqGdFZ6-;53!F7?qt~cjHN5+I{2w5@x3$2KpP6 z&HxIg1s1VUQsWJ~=1#cYA;6)886Kue2nkLa1&zPGRIXZybEw2w^+Uan?V$3A^G0(McM zbg)};B$?wW3|}}vWsqs`U|)Z*o8scq)(iFrDBd(SgiQ0K`{_U@tM5P|bgB;2L4$-O z`5V%_=*FQ;nr*0s9VOJ8f}=w9bVam-@gRUNjSWD8eSQ2`_+TCADlQ&;UJ)apkX047 zw+_^iWCbx~Fli8VxH=pOBL>rh&`@0wh&Gezfwv*-`3V8O=|H{MY=1lg!QpV=91NVn z^hBU=I2;0rMxfC!Py@ya@nZwQFh7>U3d9c#0*yss(*4n95!ABbSUAAy1+5x%~N-z`{dVh{-O)1m)p!LkFJPJ|7O#Rz0lXv83z zA6wyf2rA{5y?-FnXEhuu1wr$n`GTq}a8}emrZh4kS^ctDp#Usi{8z0&vj3sUrhEKN z)<1k(8CebIcSk_;U%3C!{wwxXWl)Pm!V?&jz!mpQ2s+S}`SDZ+g-*q$B0GVx=v0Y3_D1quX*(?J{@0N^yqR0<4* zLZM;m07et0g~iffR5TU`AT>0!Xd1w85SC0jn3aIf?_RAyQ9&q_ho+VW8iR#-V9*qp zI>rMBBjYr&FaSda0052FqWtuRO2O}8Fns}VIqANDCk^56=eat^3gLKtD-#_k8jk#1 zV&wy{JwO8;C<&lIEN%ZDv7`IaY}vpHohU38iPq9Y<4{N~Ewq;U-%9p0CJRi(6;Kos zjzO=EtgH(jj0R*DSV>e6U{wwVgEwT-05*eZ$6)yAKv#T%tXTdUCV~5j3a|kJz@~wq zNHiLc)WW0CcHmt@9j}2>MS_yw^chsTN67!Cy|Q~C+CSFZn9c&{4_Ouc*iyE%fFEx^ zUVZ4RdkF$r-4%F%@*@Nm5JaP{`UztF7@~Lqex5Y&`1qNxf63|pAs0aQw2(Bc1`ME} z$Y45=!6ys`14E(FR3sKmH7&{y@_wPS7#?g6z@+JWf;@s;f%&=06=cVcl~VbqGaN74 z$})hA!H`-Y?0;tr@iSt?%Bk^J$l8eiL5lXO!f(e6X!k<~9$(;Ti1>9J{v-`_`2X|s zb20uur+`5IO!BYz{X^Fuy8aad|4R8!cKxC2Uor5nl>cPc{~28(e>YS#Kd=nqfbEie zT5tu}Y6+0dj0jxIE8mN?dC8ze*x%TR1^%10e&yokN=ugkg@SAo5>fEI&>8_Oh5+Ar z4is%-6CK%x4BwTyoNKjOq){OpI@=4fQY|_}ic5n^fC)k0POEv`u3lufywIz~X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T55f6fL?WF(!02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002=*L_t(I%k7de4geqs1F8T2Gfqv^paB_(iMKJd9XQB> zs;X=Nfa&%Itpa40-HT?~7&My?G)eHo(O2=$eA7dEM&ST%gWs?cJ)@FP=Q=$*-n2Xz T-IaOO00000NkvXXu0mjfO40@; literal 0 HcmV?d00001 diff --git a/resources/OledFour.png b/resources/OledFour.png new file mode 100644 index 0000000000000000000000000000000000000000..ce393604784cce456920c91f6cb00e9e4ad20c40 GIT binary patch literal 688 zcmV;h0#E&kP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T39V-z)k`@2}02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{004+dL_t(I%hi*?4#OY}LtprX;{8S_GLCqjA3SMSx)H`Ey|Wkl*&>R#~Puc zY*~twP?QiA5>hC??`S!l>wSOMd!6fg-~XDq<~!f#UOxAIf1dk!uID*yZ)+hgDklm8 zfy9ZHrVhaWdf+E3EC@WOT)FKakf_~WCl{^*IUF3!X8AG#>0oYTFda;fVEBSS5&hTB zFv4C|NgQ0^$kYjR3Y@w#lQ~%L9{YPX&F`}E$pV9=yD2Ie0wM5y>l&}Fe6Hy*tTjG1 zc-Kj}HxpXyLY+^PD(NjeZ|C>&Q?1w1_8ZrG6AKr_@8{k|dUzM!Dux$vrlzLnz3vj4 z3|04LRyDcqt?ufNsk>D-I4x3Fdw#qUTopjqpH_G(wf1*+>Eh{^mFmtKtV(Qi9lI9Z z+U70R_$tlwaI`tgy7eSy2D@ol@6iMM&i5b3y(U6l%zQ@nP*c6@*S7fyeD@$}u2}8OoDHAE6t^|IXWtc2y;=XHV|&)8eqH$X#gX|ert79YYoS}llabNQXWb0P zI%ePI)k|So7ZTrX+I{wX^ZMeNxzpnzN(Q|%#m?z79-lhqt675s@h80M>lV^tupKbB z_K%REvwH(aGm%Awo9`E+rYU9nD;f^p)MAx%NT?$weHuF>L$|%Xel+ODSf%?b+|%24 z$4|lK>lB*eIXgjD7T3tgqn}S!!Y>>W!9@QSZ-V@o51rC>zt6sNIIGFF*?#(T`_`1t zqXW_5&j;nKJ6)@n1)zVnX+11yz(4Qhzq8}+(BepO8|~2s@%jvtHD~4Id>T(bw>!)i?RbKu{F$WuH+Jv)QP3il2^J5iuccDX@g&p!-=t+s&3I>GuYhM1O^(OymlZd9$2?83 zAg2qXr7ByvF)vTICpC-AQ^ep{{M0igR{(Y%p3oiEd`J>#y<2$|OJK(zg_aY2o z&t*A(^lZMIuJ>vzBd3q=Lr!|>ch=gW)q2@u2d8LXcT`R|$pDY}C1S4*-&+-aTE^!*xYbKk;okQ}S zdfiufSn5*#NYxbhT3ha@laS45ZGX;yR;Yt^rqr@KLfevkMvAOD95 z?w?lh(d}2K114p|d!)}Seu%j<^yJ1He2Q_!#&&nkB3f%)TBvNR#g^CMT_ppt2Q$m>U;0_?Dux1 z+|XbVT+U)cY)b4jR^?BXDfXL+QE0V-(e0bgh!T?zxEjY`buG-oQ_TsuIO?d~={3#J zaybFv=TW^P1sSj02HWo84;`UFLP_WlMZ_alC%B^RMb+Tq*p_&$s*9Qr?rG(-9q$V^ zApDC2-OCi@qYyTXQ$YoKDx%46x0Q=8m5Fb8HnVZ)&7cHcJhXHcPZJFZkC1AuI>K1y zGnxIsmw@WSNIn(skdaPqa8Qa2U$Q&UYH;bW%0Qdypu(*uJr!!Rvm{Ro-XjWSIQ0LF zzpM7>6da%SzWF6fl^h%SXrCgClw+QoKdzKN+N8|1V~r2{!kk*=1HI0UQNj&Bs;k&< zFkg_^oIDY;$=V`BwJ71B8*wb4NNUWrU7J$zB z`b3%dQ~l`4xK>R{%7OPMy;RXwHYxCff7BnazF(6vuDjbFeec0dKC>H>aeL?vx^Lzp zFP@O_$#pQWc>Q{yQ69Ii`_iWUeZ(pUr(Kic;bGk(j1 zrEpRk9xs#!#A7ruO6HHm2JDH${u0WW+c6p%`!n9ZKxEwCX~m!G8fV=9I8h@`+_dd# z!g41ou}sN>G%o&Hcs6G4VthR&QbRH&|ClXHL{VE)=Am2*KWn*Vli3hW(Zk+rw@l+P ze;_;6ZNlaWqcFFgW;4s6WU9`i+Vb>5k!u^TQCr*hhbBFZ+j6Q2_RQ>IvsoJF9{$~t z6WWeRLkF?jus5Jz#y6j`5ehsJ=AY(+)~CcB3IR_$TI|wW77@%sX`eKf_uSSqJelTa zns+(WO2V(jBYl=p@?e6RkZhOY^7g}N33^j?X>ybGf>Qmnf$mp+Bf43g9dD@(lO#cw_`u_+*JRmF9+^tdLcMbp9*+hLW+ zGAn0W{!h$8#T}vA&pYvBuc`I+-MCzz8uyC!yi!3cdlibBno)X%NSkqr*hx7)g~|`e z%dumoCn8U5EZ?~)Ea}m5?B!&%?*p#&iaFo@<xgvl(-;)*`zGa^4x4qr4Xk6y(D% zNjfDasII@OQxJU7vg3oZ_RgS<&?HqazRb@`=)T9<(JtM3Mlp+_Y2~|i#j2^@*r!i8 z<9G;T;`wR9YFmD8MBZhKxb8WwM}Ze;*cF@^utBsWFz>-)vgFyYHjO<8NLo8SF==U6 zhfhDTNt}E6R=MQ3?fqd7ZADbmi+1~Y*AqrB^-tc#Bhsag&4FJZjh%hgbat9M9zR|8 zEc(9gP^b(?a`1Yz@O1B1!+9I^q3Mi{nsuM=J-(>fWZ?kWe(}pT^QbFNPgF9Zicbs@ zk@ozGw!6@dSqfXO<+^>?_O8$=CVtDsVycwKixv=`e|+H!$=FZhgja=I3=Ayw+IhaD*O#ZHNfxlhpSgu+KD0&jyB?bfsB z&MV(AY)$&er?9{M_ni?h#MN!HHiJNG{$QAx*b_}mz8@`t17&7RGQslB4*5oJ#|kq| ziLebJE%sU3E;1oKn$j1HFUz=vBC6-sWuTQcV8`PE>g!K5792QQRwPjr2mZKbczh&h z9HR0Cu2G&a|GY|JdWtYLxW>4y9bFh8|A0GSqcTiUt&`U+Q+Q zJ+=0nEcW4*$YDf@i$66t(Y325AwNN>AkXn;(u~V!ZKJD}v+L}+<%1V8Z+`Z3T=-Zl%8zX{}}Ia%M^CdLj}Rs&gK&@eIvATVCb3Kr&bZThP2QP zZ~0eL0pYiNi5V}W74&Ui3cNGqci!9PeT84=d`_>M+lcV^kRYtcJ)=~}V+wS@?oi;p zqLF-!k=ffzvN4XEeydu$x3_jMV+j`n>h9U~UJ!>9FkhEeH~8XN{fEIXgokgZg>q`x zHAgy!$AK%rUIuW~cOlu}sVpXpOk+{#un1-_a1{W73=AWJ$6%** zFoQ;bIAcf%Qm_f#pJBO|O?TXD>qOlfK-Hr`47ZCKMBo7cCY?(LM=%3}IQR$xWEB?= zyz|6x2zb?m8$f`#knF)GEH)jChM{2ys96Lf3%nmXZcK0o zI)@d?rqa#A=s{eipCM?}AO69i?7-D>XjC{okj?~5IY3n8-$GgtN%lWHcog_Cn8B-F z0NH=D4oCIX#o%aI9Qh|GVi1Q*4x-X|PyjfL0pQSd zktkgXjt-^i(Xmi828)8?u)Y{51%c7kA^T!<=%}9|?AQ#TE6IUBXT^h}0Z=#!7K7H) z!9jI&uox(s>`R4`X*w7v7Ky}Sv2+X?q2s%n4ULL7XR(=NU^*F0vL78D9OSpE;1P~D zvL_NCC>Y`|iG3iM>kBv#AS5ysZ0Gow#)-kCJ95c9I)NAn6iyeVi$-E`dV1KujCRx6 z9H13>phyHv2eqo<%?lo=24I%VYg7PWRSpz`H(}GsTo&7j#R?=qcsYT2o!7s|faE887LDN>`Jb$Ls|RfGbQ4^xqXXN=w|@ODXZ)L9!07oR z5EMNe6oV#HplDs-O%I7gL1`2ml7%XE{4Ya*!M}q1BYuC;^^2~5#K1o?{?%Q-==w(t{3GLE-SuCiOZ2Y`6+H+z z289FHB_A6Ca4QB0QmidZK`Xp(c3pldAQ1_+bmf3R>!f(UHK2^l-vFTymq;=b8WR>1 z#6n=_MCpJ?mTTt1HDNJ%=W@{MX^~C?hcmeTVBTrbdA|e?oBJurW%Rf28#)V zFRm1iiO+yH8Fk4;ZRq#tXi=>(O3`vTh25hN=I!XUICDf`+X12WW#sAFm|IM_NUsHu z>{KLfDLa`UdiV@x+O()ZD@vvJ_lD!~*7nBRZ>j39X+N#@_PSr-S|A#TXl83#Wb707 Ef2>OJmH+?% literal 0 HcmV?d00001 diff --git a/resources/OledFourWhiteT.png b/resources/OledFourWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..e18c2e1c8b05b67ad335a0b51c9fd5aba77e558e GIT binary patch literal 634 zcmV-=0)_pFP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T3D=}q0`WXNK02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002@+L_t(I%VS^|y5Rr+|NqHR&xov%k&%&+6pNTBaq*~T z5&rk U*4g_h;{X5v07*qoM6N<$g5b~wB>(^b literal 0 HcmV?d00001 diff --git a/resources/OledImage.png b/resources/OledImage.png new file mode 100644 index 0000000000000000000000000000000000000000..d0c19c2072e8ce08d4cc81d1ea05c1dc8321db2e GIT binary patch literal 365 zcmV-z0h0cSP)wU3n7JIBc>7yODznV zfC+ePHn=Ps$&Z!f>-O6_=iZMQW?-QIub@r>MPRH?)+?|V)N7ysyaQjo4hD<>Wz@QQ z+HX|#L~TUz_eVy-ygJndvjz~g>2?m>15fH!HU7Fm@kcdvF7dCF++{K*y$c~cCKlv?<3vdwI8*a&Bdg8z-ou5rr84K{^<5GzZk literal 0 HcmV?d00001 diff --git a/resources/OledInvert.png b/resources/OledInvert.png new file mode 100644 index 0000000000000000000000000000000000000000..b2225a40bb8965f6e73374f2970649c7cdfad749 GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz1!3HFCgzU0`6id3JuOkD)#(wTUiL5|AXMsm# zF#`kN0T5=)y4J}F6qGD+jVKAuPb(=;EJ|f4NX*PD(aTFMQ83Xn&@(dpsE|1os3^_T z#WBRg`|T7*t^*1@ET1QQyOjUs|9h|LtJKYv)E=oaGjB4i?pcww!sLsGbHB-#8*+hj zbocPa-%OXEcZ;l8B@ok=&0aRCwl(q61!Z2_O9io0 Z`Ktpy2RaJ}wpX=VUGhR5XaR$#tDnm{r-UW|pzS~t literal 0 HcmV?d00001 diff --git a/resources/OledNine.png b/resources/OledNine.png new file mode 100644 index 0000000000000000000000000000000000000000..7819125c7fea38c59c467dcf3f8ec382dd27ab09 GIT binary patch literal 637 zcmV-@0)qXCP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T59VTpbI>i7002y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00310-zZOuZAV8y#4=1DP{YpsE0ln9gTRpG9Z`dn6vy#;HY`aU$ESxd` X`A-pDks&M^00000NkvXXu0mjf1xo-r literal 0 HcmV?d00001 diff --git a/resources/OledNineWhite.png b/resources/OledNineWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..ebb77ebca01fffd593be285812be5252a02a647e GIT binary patch literal 7575 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 V+yBBSJRDdNM9?+WDb#Wa{TJ*5Ws?8^ literal 0 HcmV?d00001 diff --git a/resources/OledNineWhiteT.png b/resources/OledNineWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..27121d6f8c118495a39f6421728a891d7c720d8c GIT binary patch literal 7331 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 literal 0 HcmV?d00001 diff --git a/resources/OledOne.png b/resources/OledOne.png new file mode 100644 index 0000000000000000000000000000000000000000..afe8d284fb6e35ca5e0d5cb4fd38479b48bcd5f1 GIT binary patch literal 5965 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 E00wbe&;S4c literal 0 HcmV?d00001 diff --git a/resources/OledOneWhite.png b/resources/OledOneWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..e71cb211c5dff49a2552e118958d2a94624c216f GIT binary patch literal 6667 zcmeHKc{tST+n*%6l(gtrra?lpFJ=aHVyxMzkZ6|gjKRz>#-7t6*@{HAEF}^`i*#h^ zs1zZE5|S+?Wyz9C<^2xTsq6Q22tzjEM5Q9=y-9&;8nK_HT_5JxA0Jv9*O!{;&C-T+h(>;pi7AT|>M3F<0N zbxu|*kf+Q$8zT>PQb|W&Rw-8v&HBS~?^R%mTr9K_T2TPm--t#XeKbMJZWJ2>(=f)$n55^Fj$`<|8&8o0mY+RE9~d8c9D z!S#W!I@T8V41S&oy0Lb8Xw>kUc9U3wa-5;ztJ!CZK5)@eb z+eSB4`8JoRGfI7M#{m)PUd9_{_THM?FV>o5>(^X?$0k$@CZ!)|b?Ev!yuRx@RaJSb zufe5Zc5Oetn6R}}HLh~nY51*iP}f@|JmqcKC(oyH-EM|dx7O;}&2!%Ad&*L4VZEn9 zyoa8mOLbyi?n=u)?nUABn`ED4)!v7So>OMHysRP1&2}5>?(yp`_EYBe(WV=vca2BB z?Uz|&R`|yXskjw=ugX(wZ>i(_z*LpP%@{)H?}h+F%3+YPr=F*R=Q>2pgb20y$zQzf>}QvAhDu~VZjTyivLb^g4$ zfs8`?R`XjLs@6hamYNy9OT@ebHdGun9jGRXA76H8eaYO4u*fATC$fpJ=n{(j`sLnJ zzxlEXZN98OS9rw3?6Hz4*VE(pYC7x>^)kJ|b5>A|G!k0+n1n@Cez(G_%-kC$5OFQq*7UA5}%R6jJ zUyvTKBpc7|sXF@L6(h8{&a36G&>qsLN`P!7GAOlr7X9Ms-Yku2M((JISHi_>$HGdV z1R$d22&;T6>qZkTJJ}bruX9rRKl15J9&6;1^_J!31ddxbvMG-kuvQ9?QakLK_ukB8 zwq(b%+0~Y?6G$bkcSmjUhMKVNQvJ6W*O0I2GYsT zSaqsqbBYJbvC`I2H+9s4&%PwvL5Rp4Vgx-%x2??Yd2@9RN7pdPQDwE|y&$IR=zL7= z*$m*@ZocE}{*az1SE{Gh3)4vbyk?c8(l{R~auI(3V5OSdza9eupOo&oEY`6mitm4c z6yT}{U*>&a-$^UIk;4r~FF4N)l^+=P2yxfo_4=uZKfzbZTN!yu?w-CniV+bQ*_*eY zJwB4=WD#{NiU-6ST#69qK7Zu3EVggAtNMn5<>}HP$@CpX7do|?iK7qID(P+4Xhv78 zdfHPFZ}$Fq<<+`YZN7|HzHZ*tlvu?*qqpjCO3MA3*GJcVG}~RT);Dp`2m0cZn9>9^ zRolEf!A5*#!b2k}(x^54$yVvz@Alk8+wnD(+8s{WEe$cv?^LaH%Ou>N1@e8>dYaog5IXxWg4~=6^A>?;{G|1j}F`1FCcD{ToVc0m6ftbsOZ^Z zjrcCSUTp0XmF*&Exz`*(%GTBKJX$>Ln9T8wiI&WZxt;YxCr&>J_^9{L(!gg^+h)I9 z*NH3%h7IR(hAltE3w!EK3~Tb8l97FMvE8T5@{M=T#ptvjJftb>YFU`k#@aPiDd}oF zxZ{x`**y8RR6%2rW&J?h8_klYoPope@S83L>(8{UUUB)X-mdr;H9pFey~(%eGZE?V zdI~K`BIlLu&L+uA{=>)o&F$2}dFMIJ4t=ZbmetB@WI{Hzjjv$+`|Lf*-;Ybk4R@+UZ#uG^&-DI^=t#&Qe((7NT3rd+Nq|5=J) z)+*tuQ5PJce9(Ux?4!Nf?&6G=W{o6`=C0^yO;7O~)Y>4Zbx7HVetcS()MuY4$|kwy zgEUph4z*F=&`^u{o^{&6lR0Q;x4_7%0*`FiDVtWaQciVGGv`4KCRm2Av?EdAmQpkQ zo7_$`FHm}Nzi1N4te}=|eQQ+u^p@FEM$HkA+>K_Mu9j8=i_8@CzJSiCLS|InZ~=Bq zmI^PBjDE_AnGQd~=gplSvVN(EKU!>UZ6xJ&Ha1FM%D^}#XZ0?*#tQSD9 z6TV~sYBN`Pgw2l-yh(uJD8^;>AYUFp_Zp<~ge&0qpVvd}U zz+Z2q=J5!-U%qa<)ZFrPd+5_Fl2>QvrIw42yR04{yvQG;vsI4v6^FzbCCONj{PxA; zWF7@sd6lopIOK9MF@PfPbLeE@vVB1U^hBgXbG)mnj>G2B$e7kow)Y=I9KIv1HQZjH zdLiJ+mZLkdhT6W47LH!I%n~o#6+M(|vT=h2=iTn8N!>6}K6xr`UxF&Tq`#eci||T3 zZ$<~eH8z}NW-rYe7Bp?RaQu&kI1{yd$p;)i-(eegbvRoti42Zi|1KbQeeWdP z^OK!!o{C-gWTk>Vc19jw-IHSy?EI`e{Nm>?kMz0k5(s4)h8aO=(}77_wIX9Bx(h9K z8|QEJKgcnkPH9CHmclCdo)luODCNNE3thkP0Ffi}X(mX^)+`cv^ z_CfV`;F-+%TDb$4-{Bp{KND>3&dY00eTgmY3|{g`r=?k^V_rmi>XE4ZbKS|2l)2Q$_BlHR0`ge^Z1 zZEu>xyX@*Zs7~ILq{K1NIA~|FGv2%An#GqKPg3dWn+Y?83f&+3(?)~wH}sw^F~!e| z2;4>jF5FD;fIyZUW*ZsVnj0B?J1v1{q>Qix1G8JdE7iF>Ts78S9{Gpe-JtIMWhu+D6bWqw!2_sE zP9F5UW6lrTqw=HF3eP#z9DnOHR8{A!v&(rZ>r=#;jGD_${^_^O_O9H19BR02wMG(E z&7#OUCPHEKuHO4d*BcZ>gsTRMzxlL??B&&A51OCBHJ}{=hH&x$EmBZ@K7&cNrV$d7z`YTev6Oe zV`cT7p6mBj1yB!U5Y-2XMxc-!4)RA2KY?)oNb)tH|LWoA2wnn^_JAMHpHByj0|2f- z?MDg*{ky-9Ki_*H9R?iIcq>{%J~6b1U2L9zqFNY>v-@7bx~mNCBJqw^%>L zCLCEv=f^-m_wT$vp}*z600vQ3R%8l~?k^0_oMHeI&QE6W=xhdg;V%)5Vgf`Q0gflo zaBv)kiG~x=I3k=*(j^hFbfzvEgZqKXoa-l`a_NAO3M5CcK^}mHref$+Je*D-;z0^D z22Mi}iEyGW4v%Bv=>(Ln?hh0;d^XsXRPP_75>hchDm;dO1?W@^oJz+4a2!e(4JR?s zG&qKir=jRnx-OH6TA*Uk$tFBLhYFUH&7raYqz{+1Fd!71yv^3!0ER&Yo+_5zw`ncz+s7C&EXglfCURk!^25* zDgln7QL!{#GzpEt5PoL&<1qz+R6ekc1?mWD1@`BHR?tn0mD2hd9_RrG%K$0{N9n@R zL`O85jK!0&x?dGT{`)!N(F_cR4lv;)G@S;=VVMj#l}JRv84MDUN!MkdiCFBh$N{@>)1`uoxdaKWQqAb2JGGJZsOp%kNS zH>E)4h40hV`H7%K(#Oo%4+4>1CHyRbq-U%Hjp71xD`W8&5;9_V)H|t7W1vYjwU3mMBJgo zma4+~XVclmipgg6F)t6!VD%%9ZESh?+}we+6vE!F VqOQW=T7a|=b7N~tiQ(Qu{{v&GehUBq literal 0 HcmV?d00001 diff --git a/resources/OledOneWhiteT.png b/resources/OledOneWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..40b86f7a605a2601f5e13ecacc384988c3af4b2f GIT binary patch literal 5387 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 literal 0 HcmV?d00001 diff --git a/resources/OledRectangle.png b/resources/OledRectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ab148e3e6fdaa306284fa9e5f62ec3969a3c65 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz1!3HFCgzU0`6id3JuOkD)#(wTUiL5|AXMsm# zF#`kN0T5=)y4J}F6qGD+jVKAuPb(=;EJ|f4NX*PD(aTFMQ83Xn&@(dpsE|1osL0mS z#WBRg`)!Y+5Q70v^RNH@>D!o0wI*A;2>4724v>PtZyU2Otasec_E+jl_6LqBY+(35 aMJ{oMgWKD6AZ#26t3hy!2FIF#q zHdo=j)k*Qn|2}RH=ls@|(#_bi;9`>-H;{3rCz12^htuy|Othp=lsB_&_$@D=b4d2u zs;>`O|Mh>l+i@&`t4{gCU!LDw$8M3TyU0+Y;XY4E{UN<Y`H>Q1oxpbmXqd{p}6cfXxCSWJb0fxzJD L>gTe~DWM4f%}te0 literal 0 HcmV?d00001 diff --git a/resources/OledSeven.png b/resources/OledSeven.png new file mode 100644 index 0000000000000000000000000000000000000000..648873fe3cc6cc8b5149a7a70d289e6a49bad3f4 GIT binary patch literal 628 zcmV-)0*n2LP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T58YUw&tO5W402y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002x$L_t(I%VT7~1pgUu14d*H6A2m_goTCw<1~p>n~Aa! zmwHAtjf})<#O7y`z=+l?EG$g6A^&Ntevp9w#3skl literal 0 HcmV?d00001 diff --git a/resources/OledSevenWhite.png b/resources/OledSevenWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..aca8a8a6d32cec2800ffc47b761df586dcbe0bdd GIT binary patch literal 6633 zcmeHKc{r5o`=2&zX|qJmF-8;`WA=riEFldOvW1%E9VRnIGh;0jDP<{JmTYCOBqfm) zNfcTvm4u=!Crcf&r1-r)I<0iePFZvYDTvsn;` z|Er=DcAQ$}s$=uxvIa}>-b9%f5i7!MpdBvcvO=rAUpvG*v_W#6T{V~A0~$mrPT z?)&6^L-|0L@&}2x$sQwjdJMbArb6!=b{i~%mV3JEjj6nrUJhC?o7Vf;H2JxIveg?nmA+KEXa6uOv*xhMxtkk;#=mU5x%J#|ZhX)ANoB+L zl%nGC{s5o+n)Ns0M#bN%UB;2?lcv)3guc{rYw;%CEcAEULSvYHI5& zAzA^hL67yy|NTmucE3+_@64XwbS{d?eQVT{=vUG#W3&HNO-%ZSKlESn6uN}X+?$UH zudDA5COwJjUfow);RGrCvR#XCAmwJK!3iae!nd^qqHI{mtKP?DpE-D!lGc|)$$$J7 zSrN}PJbdnMdil-gae^|JgE08jM$bG(xKDNQves?)NAwE#OTYSu1Z!Sdvi$wAiZ}bO zcez*!5Aq($$vi!J@}7TtM&r2Kku@bZ_D4xW_s2{3u9FAGt_zl5gHUUQDi_9}Z%n@4 zH&-z>KVs~26@T~P=&sMb#hq6VL>#A6RKqB%R$Zmb{<*o{NCqpp&UBN-S=b(DDGO~x zZdyXdUKt|mq2_?gt}7LZRC;XOPMzGgmW#zpzMS2e{@lis8%^1$jSuche(F3$o_Lv? zpK)lYeFA5mx%Na9=FctJF}QClsBj{gOrorVO_=cNNTpC0I3Cx0Rn8XGrD?VW$)$W(FbpVM-CPFHfbwb#@dtXtE~M zE*k0YI2DRchlzzABqBP=C6%%0JKsXQ(G|+Czw%iXyv_t#98wZ6p_@QW_>>f(N2E3YbB?JN7pA1*hu8D86cQynOZB zJweviYL$UE;v0^1W&g=rJ0pX&-ps7bQLA zxMJi+5<8vsnye_FFPF%9HK+X zc_rjv-9C9R^P<YI4o)G;ObOZerU2wmr|gg{k-K{qVs1rW@U3N$9cW*=BzKrJII(AA87i z67E;1-`Rbcf3(ZA;pR%)r~>tULvHB@9;A$pZ<5PDV=0|gFQc&u-cdelmsL3Rn?m>& z=LdHA)ZvIHlBR7j*nq%-%Tsr>0!ydNogA+TH=LmEJaakv9kX=Uz99V8iR_FwN*AG; z_vI0fw#x|fd}oA&wkCZ6vlqh+>I&BodRlk$`?=b^~#2pOF12-B2YXOf(@ z?V-Hbam{K6weYhm!}2y&r;wlJoguY6X{jsCVt%Z!E*;!Dx1ulzKWW;MY=k9A1^$8W zEZj%nX%Fn#J|&S*F}w58#fgLt*Ejdi=6Hx^`&nM zGMLrGZ=3&+vDZ6;GcucPQniXQSKr`l7_(Ykn5oreU2`(6!~@eCE8)t<yM^jc@pQAs<2&cje`ef0o-P8#_ZauslJ!ismctmZYhcJbb3ADEZT zb}jl;)%yFV>UZ0fUs#`DbgqEI}Rn98OOG#VZ|Jd6}#`R1IrS-$1 zJzu*#lwW_*yVWIAa+Y8eu_N2t`9f#Nfg5;rndcexV?<9#2=%aKX&qwCWVQUc`0)<8fUccZ9jgyi8bDFf~05!_93=f@%;ZNDAzaO$P^d zWraK+Dk8NbNC^H{!(n>bgQKWft$u?8M4S0e-=eNpZ6WuYdT!c|>*WQjA;vSR=3uUF zF}E@Utdmqa*1ei#oTv#6s9<-`R7QRaC}^^dn3H>QPQhe8o0@WWXiW%*>bSpOAtXdz zYy4m|lfB*8TD%$F%xJdGOZ3XlhZ}7CX3*XhdSZ2$!KmlhOD zHm}{LT({5mhN+f}@0x=R)by>6atGVAWX~HF$T|DyR!m5zVm4{&#Dsd*)SRxn7#4Z` zip-Tz==74_!FM@>F!ecvW@*IdH{~j0qvX-JX-lhvWSZ+Yu58ulhGuk^u6jIspJt!1r&rL6 zjMp*$+Tc^yhkv{xX+?%2uC*|rSGUB`gOMBU^sFr+KSJ$dp6#78pB&%cu5;3Ma2n6} z8h$?Q&b20=)LQfXYc0<}jke3FC(zV(Ua~qBE;~}MH+{n8u8B^#i@Gj@lJZAZ#35`)Lpp)q-MK*yi!4ff&?h`yn}H;v&5 z2%&Vqo$W=2jaA--LD@_)%mGW$rFa_y9&GbKK42SYWyc8gWDuD!Ljx&&e-cQ*1%x!H zKbPYrAo-JF3%n%ozsQV$K^GuGPcqDrLWLUh_y826gVE82oBFeTQ7{83s6L;`BH5Vi z_)Y;nkzpP}p*IPE@bmN2@k8tI`0fZKkw`@7q7Wz)97Mnc0bW9yKio^8CZbs6FaZP% zKHFQ!=6OLyoHRPmM@WXjz;Wnb`Ek7|lppk7g6}GTdLaC1-Uy_QE`rNN{OlnRn)-qy z-xK<;9s)b?l?7n~2zWkx24Lz7cnQ^hreHFD_JJZ*1nz9E_ktHF_CJt9HtTP({*jw#WMMi#Cjz?v;Qa^suemRPK@^2TGT||NMCnC+^3=9f@W6%TwoQTCU-~bkf#R3ErfTOd1qO$N32x(pn zKtu(S>##u{JOfF?FwrzP8i{1WF(?)iPRF8{Z~~rAATStc1`~_^iNc!C2CI_B`8g{Q z6%(YQ>*7&FT~T-x7753&kZ3raPA9-oEIJcQN9v-8Xx#-WCWEwt$LG?(h zizlG|2H69A0a%J6Qlzd9dT~UwE+lX^pt3Ykp@IYp9^hn1#(aP#* literal 0 HcmV?d00001 diff --git a/resources/OledSevenWhiteT.png b/resources/OledSevenWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc94daa1149288472f0f7cc07301557a42cfe9d GIT binary patch literal 636 zcmV-?0)zdDP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T58wBr9+@}Bl02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002};L_t(I%k7d;3IHGoL$~gKR6T?JMMXtq1X1r#bPjYx zg_#*TkYrwmSs?(3NYSUqdCIXj&s^BtJTMAy@U&P}*EX#F`7%;GJ@@(XJSjdB=lw*v WjW&h3W4(a@0000X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T57aPveSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002ixL_t(I%VT7~1pgUu14f)20va*&Fj8VODK=s?=|3Tp zaRDX<3c#plGNK9=&y2%AdHyFidHx?wVM8f-{%2rdU>vyQ2>{hR5+Kf0#@qk^002ov JPDHLkV1o7!`y&7V literal 0 HcmV?d00001 diff --git a/resources/OledSixWhite.png b/resources/OledSixWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..b9167b56db0137f03b626b5ec600726f3b4508c2 GIT binary patch literal 7650 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 S97k|9h=HD|ZlU(!(EkBQZ-4{< literal 0 HcmV?d00001 diff --git a/resources/OledSixWhiteT.png b/resources/OledSixWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..4b4e2ac8de1abdd26ccdd4b44e1dac69dccfd97e GIT binary patch literal 7402 zcmeHKc{G&m`=4ZsNS36MF;uA82Q$N{Y}t1aQezedW0@Jok}XuWN|bCRWh+Y&MI>7y zlI%hvdq_ly`aRU!+k3v>bAIPN=lA=s=bUHeey;nvKKFHfuIs+feMOiU9TDK$!3P3? z1ax&Y&4B+cz$?Ma4gAg=V>N+5o8S6bShLKCY_JEzol0}1fLXpC6fnhyMg@UZwS9v3b8~|M&|_G3{S@>Y**;1qB`bOX5qVSHAjJD96^X)T>|M zr`gp>9lY=TW>#u;$^WpG8l0s+IMUnm;%kV%^sHM%^{0S7;S8IPJRPGC*@8=XABTfJ z^kiI>T^Tjz?N@=-a%DBDIwJ^abaITlRBC1^c_(Jknha|Ug z!DZI>!GmQTnPZ<9AJ+oDO06)ZmdcnCEvX0l3R?56%&%IpX9>^M^lobLZ?e5*n{!rS zEYkOQp3gPC@*=}C*-*RjM;~D&J%@P>RG!^`;U#u<_5o+&EAn&v@WqgGRnP>9)SXYi zFpCCnO`hpG$-8`Iy09Q{S=}RAM5F3a)&WLIp5r6wIwMjRqGMB3!=}QZ&q_gfPxt0M zTWw(~Tem$M!MuvC=9}pXq4{Vo(%XGDulNK7=A0nxj;?Rw;!2(#wMjqnWvkscY%dy# zvM=4|JHUSuQgWVGX7`qESv5WN^!Ik#sxETzC~C6R9-ils)mGm6ifdNng|td{vSVRN z4yu5};k;oyxkMe65jjRnKEJGa&gkqnUa!Y-~2`6iJ!2P1Mz}Et zMswb0VZ8%6@67a|d1aF!v(^o5HqLo@_h_V%r)@Et^1QkNwh$KEned9F0q(cx>T@zN zs*Tw<4V71As~nSjuN-r)p_U^EeO7d~<{^<$D(zeCV?xiggYF4nz*XoN))_Ba@Yn?qrwK;nUmD!* zdj39>JvWElyb@YNc^+sZ{Wp_c&<{&*9lbE8qO`eH@X}+mm}gzOopnl3jbozJa&=H!BFH)fti1|&O2jXbb*I=`QPW()HJ zhy%Qot=hyyi%n_A3l-0NBU>~@2(!z>5S14ly51(-$!XUh5ho$4;(=Pl>H{q*?W!@3R!@~6)qNHxTf z_l&%%85gCUemOmohSJsaEPJSH!e_^y&=OuRWtuQwcw})eJ$dlcC5Hxk|5_bNp;+;k z61~a!BxeyzTd9QX=RF0_6J081;g0roUtcgr@05HemyR@lamb=FSXz~N+m}7*(TOh) z6kYIWzD-GZT$1T8K4RvgJw$^Z%@bp`G3CdjEeUUZE0-e-p1LN+I<5m4S|{S-=>44-j&UwzfG zzWSNSxYF7bEvVU)Q=V+aROh_h7RR=J?^zW;GsHXExw#Ut>vlVXH+odK4CJu8Sw{PA zw-xSt7&0-0Pu6)}H|^*el`50;c&_$BcYbNENv8+f)tu`FY_-;Vk?h1r= z@*8BRRE_J*%B_GJj#*Ec=G}{no=W4$fQi?FiFO08h!j_it&uvIrOkoOjSnJD4O)B* zU%vmWv!6dz`2c?~Bv02sK`|vPa{omR9jE+i`-n@Dwh;j@TleXxbDQYJaYTfraTtr) zUa4tlmmexX>}|QAzrB4=gidMsw$yES>g9)+=hVtWU7gh_|S~peVKc6jshhp8)2j7XADKj5+8n$g#)z~NyJ3HtqCmGQ$*PQUql}}IYr1*Vt z{wp%=XV0WHhEbzLAy`P+Ary5lhO^w*BWII*sFbBCb+>tEp(huFOJu-PeDBaw^#Yua#> z1>$kjD_p^8yvg!DBNRh`RLX}w%US=St@79q}5d7J3r8Jyxs$Dtz)r*MM7yg{$@u~ zP+;_u(ePkGZ5u*;>Da^E64 zse{mDXIjdiw~*O`cO}?w1kV$`|!am0qsj~8w>U3Hq26EEVEu32B$8(JsUQf zPvEh)c*^^h@Wob1$#2vhHxjG15?i&f>JsghaaS;@%3_lZO6q*hZuyb%KyZ-tRHA7Q zUEg`WI$Wu7J_J<~-1_lIO{qZ&u8;g^jwEuiLe~m`->PeZ_QK_Kp@fN3K4x zL&D59Nh?)^wlX13dRY@A7Hr=YJTIDi#VEq?xUqtXPzaK6>^b*4FGHj2nLP9gOZj?h zzSrro#ZlRkcQHARHM#^sE01hJZczno5gtjn$Er_=%$@V9>kvCRa=(A1$E`Bs&Zs5n zVHMBmFPBf;KGJX7lK24DVJ<$nmsN7byD&QG^aBU{+QUV`a!z#5iB=6Ny?=oE?2=I{YqBGqjtf(rOZ8i$fEmQXSB7FE?iZ> zW{n-?dQy9`CQE?A*)<-yt?y$BGqXv1IYArGy&P!3Z+*PKFy*NE*Mo{dw9VpTw-OWf zr4a-Vb$shW>bs~Ur3q|F5954)VV7Tvf>3Vpep72T%kqq~N>!IbmqPpH_6T_POzd))jbZZRSVnT=Xy}XOe zgYaChT0L8Hgd{Efa)}fx53ifm`Z6oyk;Ro}eii1;qJ`y+3Ckgj#=KHTvEVKUbHPKe@$5^j28D^z#+L0fBf5auK%%SS6cU-G>)F}$SOi!YvP z`h3ew_w|L(0_>F5Hgnp}INqr9;ikB7>pr7KS^c-k-=w+Qj;R_*FKUOo`=wz514Ik< z%`MU1zJA)Tx<$29QjvX-nLaw{nC~{afMU0Y>AigD#nT+A85tfZBo&K=KQFx z7uI643fi-x#t3pocl3K-8)H0qgTomRXhRrHL&HQ@L*uVz2WW9p0%8d|HHUW9ov_U? zv)T0?tZ?-P{+N)MXS;&v4fSj>8!trZ0)G-lRvvc#v`bahg}U3JQAPQJ`KQ63Hw=xx zyEP8k^9?S4|J>Z*L#f#r!py*iZ`$EG)GlnA^Oabmyg z|FG-C(ucWEuop$;Evq<)-T*n+7<`RVM%?NyWbAH(9}5_g+po(vhnu^4SX?&nUD^AO zJIB&%0l`Waw&~-8Q-#&OQAIX6_c{q*_-lOWl*B2_ z*^9N1D`55D=pBvOPt;S6#)yj0dX0TP^X#hBfhF2e5R3z9d}5b|dA^6f=}!u8*rY6< z0xCP<`jy1VJHydjMh}vD?LE)QtGdz3=Jg3W2z~ul*K3>iyyN}cu>99aD#&AnUkz zV7w-VL%{1QEEfXA+Ry~7;m)9dF)$1a0oC%Md7~g|d|*`unTj{l)cy$pd=Vf{ES3iz z4rjC3Fg6B5{hv)%E!Vqve9sauolcnVifc$jmzgjRYfF2%h zMq#>pF-R0GZ;BgB=647(>6g8S7sGWu95M+`ai!1!RVJ`1@*hhc(KR&rWwAzqBaQB{ zZUvD24^0-0`Zrns@NI2oJ)GYi0nC5l{zLn(*w>W-Eki@RraQ@N%{^UB0%UD{JlUN@ zBjeYHiWCG{QIUj(643}E6obKGp*SU~GL(p=D3Y;6Di(zx{|2S&#$*xQNR%}w031dG zaF8epSrJ7>LP=DT5)?zlQlLbPA_j_8L@O)d&{!-|8S@*2F@pwFCDHYFuhyW*02B%9 zfKx)EaZqIv5r9%elA%N;2MSaPNkSoUiWmS+aUF_G!fU%T=tLl$G&<3d0{3upT%TAY z9DmqEmjFS*5PwTdT!}0yU_gKv5=mfV^S@^-XmpA>i?~K7Qb|!+8G*$h6%ja;5?bkR zrK1!E6DY+sP$U9|My=1R5t#$f5wC2owsBP{t$C7DyBxjmD!D_agwwZ~E?J8rAoI(_Y&>VAUTv*P$_i z^?lbxKem)P#q-D4k55#gis1s#lZoJ!4ip390`MkQk9WZ3K@q&QHi8=7Ji{K-Ki`#kwH1^2=EAS z1=Qy{S75mxnUepfI@^h|mIi<^C_)+fpNzqOMhss&HU0`&75*QjsIDvgcFX{FKV-o1 z1)PTPU&rB3(g275KcAn;`2So24F0pozvA}~U4Q8MR}B0sQ+W?*>p(%634= z!_qa>;u+-;;FFedfB&Eu5J|AKtXUfF^tF3AX#KWGA%od8mJ@jGwrCY1C=MtY>uMgh zz&<{98FWQhbW?9;ba!ELWdLwtX>N2bZe?^J zG%heMGBNQWX_Wu~0a!^yK~zXft<$k9Mo}2Y@wZG;1`C^!lngSOmDON1k{z2}dP5m3 z6dR*~!9dBz=pV2c>?pau&w0=D_S|!ydv6@S`gA+L=iF1b?&%ao@vq!6hg;@w%N%Z* z!z~j7IKVOXVpq#86DzP;g10Yl%fuCI&f)C~+|n3_<~}|pBk=aO9=HL`YknSj~dYM4_PUC^Q&}cUnR=sS)?hh)3@dC*{Z2t<$Gi)DJ zif9pD*d|rhgUFO;3>AAf0*MA-O~7?9g7dRoGdb z6KEz;I&ce;543%*T7YI9l}{dq{D*1ZWC-$JSf|5mZkfX^bGT&=xBR=J^8*CXZC>QL R==uNv002ovPDHLkV1ht3%b)-N literal 0 HcmV?d00001 diff --git a/resources/OledThree.png b/resources/OledThree.png new file mode 100644 index 0000000000000000000000000000000000000000..a19413aadbf3dea2e920dc2cf740c8061318e233 GIT binary patch literal 695 zcmV;o0!aOdP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T39}V$)b!h+q02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{0056kL_t(I%f*vR3dA4~M5{t>pj&r+{}Xux1#eKZal(Y? ze3~H^hzMO>exAft!d#p#%uG3F#2AN8NvO4Q+S<(2(?{bk5&&!-V?UPYP4y-p7kYp4 z`fbyRrvPyKWDFr3b4p3CO*kl?CcowSTXp_@c(&FS<9R<}>&?3ZPd@SF55~)X;NTgi d4N57e`Ul4bFm+EETG{{r002ovPDHLkV1kw@AF}`e literal 0 HcmV?d00001 diff --git a/resources/OledThreeWhite.png b/resources/OledThreeWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..8faf8fbe1364fc2e479fa4318e18938316c08084 GIT binary patch literal 7013 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 literal 0 HcmV?d00001 diff --git a/resources/OledThreeWhiteT.png b/resources/OledThreeWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..3ae96c51a5499f2fe9692d4c0ba840d2321e7864 GIT binary patch literal 620 zcmV-y0+aoTP)X1^@s6?pbuq0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T3Dgj#j`}6<+02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002ZuL_t(I%k7dO4geqw16%(8=R*(#3nVb?&P}wPI4c04 z1Gc@~x4~V3f0k3s0cV4f)P6pw@FOWn033}eJN2bZe?^J zG%heMGBNQWX_Wu~0ij7mK~zXfy_KO(#6T29S0IoG1OkCTAQ1=z5`jeU0Z0S_i9nDb z5PSncAXf4LBp~6^}us;Zi*#lcB* zXIlJE;Fk1wpR4jqeo9jee3G*=e!1kegtNAVEbyUik7WFEDyx7uIoEV+vcQeDWf@B{ zhS(YDAKsUYnJke0Wz(&$q(6`DN#M~C&X_3%F39)#DlZVrdzG*)J#LDDcu-&E1v-&% zD?Ns$GcZ0U7idv>i>yh{MbjCGX@M^4tEP?f(DVkP3v|=Agmb6qJTn*=3j{pFbPk%q zKs=Z5E$3xvju{N3IM31?%5)Bz!9d;wcxl@Y`RixMCgxoQHu$*d)@_`JrZ@1E0l}%l zHFw4x!lsOKO=sY;WQ%a#aqfAXX9$nd?=hOrKrb)d#DowAu|TF6xZZT@J7gbvj%B$J z3uKCccTKlCmi|07Es)6qciNUCs_$dl@U(Wh{^>22R4? m1tMFd>z}Sq3bfMntI-bzg$H?zxIEba0000df(eJ(Ni7Xt5d5CA)^R`EU#TzI7uGxOuRmxS eAO(c`CR+x?cR$tlSv(hL5QC?ypUXO@geCyF%{evz literal 0 HcmV?d00001 diff --git a/resources/OledTwo.png b/resources/OledTwo.png new file mode 100644 index 0000000000000000000000000000000000000000..41ebcdbf64fecdcc8696a9d7c0b04f83be373f0d GIT binary patch literal 6795 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 Tns&|u(II40Ym-8wy^;R|L2vg- literal 0 HcmV?d00001 diff --git a/resources/OledTwoWhite.png b/resources/OledTwoWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..3389cab83193651c2a2e17ac0948172f0c701c1a GIT binary patch literal 6957 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 literal 0 HcmV?d00001 diff --git a/resources/OledTwopoints.png b/resources/OledTwopoints.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac8be6f20172b0deb4813150ee7b0935af6f10b GIT binary patch literal 578 zcmV-I0=@l-P)X1^@s6`FrFA0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T2F)Y1FD<%K{02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{000_EL_t(2&tqU1bbt{`|3?;M!lsgG)SXlX07#Pp3oY7; Q&j0`b07*qoM6N<$f@}5YhX4Qo literal 0 HcmV?d00001 diff --git a/resources/OledTwopointsWhite.png b/resources/OledTwopointsWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..9fa01a317c98cb64eb78d986766b28e1d50725f7 GIT binary patch literal 587 zcmV-R0<`^!P)X1^@s6`FrFA0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmP!xqvQzarT9qb_DkfAzR5EXIMDionYs1;guFnQ@8G-*jv zTpR`0f`dPcRR9s$1J#d((hy+2o2%UcWxNW`yJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_GX|4P0DzG<6TS+yMrkYKp12Qjkh06oB_L`lcK(a0~RWd2?%@C19(3X=X}00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<_8T3FC8eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{001LNL_t(2&tqU1bbyh8fq~)w|NsBt0*s7|OxRR1jk=Sn Z000QI2n)YC_|X6W002ovPDHLkV1kT{@ZJCb literal 0 HcmV?d00001 diff --git a/resources/OledWifi.png b/resources/OledWifi.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e21f6594897b923c66bd3f314f331bb84ed503 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz1!3HFCgzU0`6id3JuOkD)#(wTUiL5|AXMsm# zF#`kNArNL1)$nQn3QCr^MwA5SrJe=pfNC5n0VHHQ?dx4U``?LtrYMU`FIw0N5A);B9sk3arA z<+T4lKP8(5|2Wi_Cq93d#Qjgu#`_r+}eENrvdkBS(kI|ksBmUJ}=-hYfh63Ihc0gOtaPvo>`S2X2LA5 cd4HphbHltNzWUv5K!-DUy85}Sb4q9e04pAHasU7T literal 0 HcmV?d00001 diff --git a/resources/OledZero.png b/resources/OledZero.png new file mode 100644 index 0000000000000000000000000000000000000000..965b76c97797f151c1ca546326a7ef66be21845c GIT binary patch literal 7266 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 JmT8>~`yWr+-|YYZ literal 0 HcmV?d00001 diff --git a/resources/OledZeroWhite.png b/resources/OledZeroWhite.png new file mode 100644 index 0000000000000000000000000000000000000000..769b3fdb564f28d0af931aa5f6a17ccf7b46d41d GIT binary patch literal 7570 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 literal 0 HcmV?d00001 diff --git a/resources/OledZeroWhiteT.png b/resources/OledZeroWhiteT.png new file mode 100644 index 0000000000000000000000000000000000000000..39d90a9fd579914d2b7c80ce6407645c9f1601a9 GIT binary patch literal 7314 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 literal 0 HcmV?d00001 diff --git a/resources/Oledrpi32x32.png b/resources/Oledrpi32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..5f4e815af57830bacad49da359e13cfa91c0ebe8 GIT binary patch literal 6242 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 Ef>0%yiU0rr literal 0 HcmV?d00001 diff --git a/resources/icon_128x64_kiss.png b/resources/icon_128x64_kiss.png new file mode 100644 index 0000000000000000000000000000000000000000..ad12c40ee718b262305c3918b563b7d792bab71e GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`$P6T3NW8THQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`*#dk*T!Hle|NocXoPQU{;wb|fr1Y_T^vI!PQSfm z$k*f`z~v=4b14OnvjGYxZg(u0|2kj( zf@tDOhP9Wxub&pvi7jJTysn(t_XC^K*>esH9-lk3Q0o2d+3Ox=CIomyy>HP;k(eO! zf6wv9-VVB!EwkiR!zNB}PF?Vz;E;{V$A|=Zp@K6TyN*qNX!^op%Z(X4JnS<{`(+pu%B@T=e`ZLNfvBCzcP5b`njxgN@xNA0=9*o literal 0 HcmV?d00001 diff --git a/resources/notafile.png b/resources/notafile.png new file mode 100644 index 0000000000000000000000000000000000000000..02abd1abfe8e841ef7128f9ad8c38276aa0286e6 GIT binary patch literal 6536 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 F{SUoaRFnV! literal 0 HcmV?d00001 diff --git a/resources/raspberrypi-logo.png b/resources/raspberrypi-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f626904764edee15ae3b6243c4de50ac28d642ad GIT binary patch literal 10903 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 literal 0 HcmV?d00001 diff --git a/resources/yahabommlogo-2-128x20.png b/resources/yahabommlogo-2-128x20.png new file mode 100644 index 0000000000000000000000000000000000000000..fcbdabe1e354cc882ccb0e0513f0659a5dd4e128 GIT binary patch literal 7550 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 literal 0 HcmV?d00001 diff --git a/tests/readme.md b/tests/readme.md new file mode 100644 index 0000000..3341b8c --- /dev/null +++ b/tests/readme.md @@ -0,0 +1,3 @@ +Raspberry Pi allows for software I2C. To enable software I2C, add `dtoverlay=i2c-gpio,bus=3` to `/boot.config.txt`. The software I2C would be available on `bus` no `3` +where the `SDA` is on pin `GPIO23`/`BCM 16` and `SCK` is on pun `GPIO24`/`BCM 18`. In this tests, the `SSD1306` is using the hardware I2C on bus `1` while the `SH1106` +is using software I2C on bus `3`. \ No newline at end of file diff --git a/tests/sh1106/battery.js b/tests/sh1106/battery.js new file mode 100644 index 0000000..9c476bc --- /dev/null +++ b/tests/sh1106/battery.js @@ -0,0 +1,40 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +var percentage = 0; +try { + const i2cBus = i2c.openSync(opts.bus || 3); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.battery(5,4,percentage); + setInterval(update, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + +function update() { + percentage = (percentage+20)%100; + oled.battery(5,4,percentage); + } + + + diff --git a/tests/sh1106/bluetooth.js b/tests/sh1106/bluetooth.js new file mode 100644 index 0000000..fcbaf59 --- /dev/null +++ b/tests/sh1106/bluetooth.js @@ -0,0 +1,34 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.bluetooth(5,4); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + diff --git a/tests/sh1106/control.js b/tests/sh1106/control.js new file mode 100644 index 0000000..72fd378 --- /dev/null +++ b/tests/sh1106/control.js @@ -0,0 +1,78 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const path = require('path'); +const dirresources = path.join(__dirname,"../..", "resources/"); +var fs = require('fs'); +var PNG = require('pngjs').PNG; + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const piLogo = "rpi-frambuesa.png"; + var image; + if (typeof piLogo === 'string' && !piLogo.includes("/")) { + image = dirresources + piLogo; + } else { + console.log("Invalid image filename"); + process.exit(1); + } + if (!fs.statSync(image).isFile()) { + console.log("file " + image + "not exist."); + process.exit(1); + } + fs.createReadStream(image) + .pipe(new PNG({ filterType: 4 })) + .on('parsed', function () { + oled.drawRGBAImage( + this, + Math.floor((WIDTH - this.width) / 2), //x-pos center width + Math.floor((HEIGHT - this.height) / 2),//y-pos center height + true + ); + setTimeout(()=>{ + oled.turnOffDisplay(); + },2000) + setTimeout(()=>{ + oled.turnOnDisplay(); + },3000) + setTimeout(()=>{ + oled.dimDisplay(true); + },4000) + setTimeout(()=>{ + oled.dimDisplay(false); + },5000) + setTimeout(()=>{ + oled.invertDisplay(true); + },6000) + setTimeout(()=>{ + oled.invertDisplay(false); + },7000); + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/drawBitmap.js b/tests/sh1106/drawBitmap.js new file mode 100644 index 0000000..76da9d8 --- /dev/null +++ b/tests/sh1106/drawBitmap.js @@ -0,0 +1,39 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const pngparse = require('pngparse'); +const path = require('path'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const image_file = path.join(__dirname,'../../resources','icon_128x64_kiss.png'); + pngparse.parseFile(image_file, function(err, image) { + if(err){ + process.exit(1); + } + oled.drawBitmap(image.data,true); + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/sh1106/drawLine.js b/tests/sh1106/drawLine.js new file mode 100644 index 0000000..8e8d5af --- /dev/null +++ b/tests/sh1106/drawLine.js @@ -0,0 +1,42 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.drawLine(0,0,WIDTH-1,0,1,false); + oled.drawLine(0,HEIGHT-1,WIDTH-1,HEIGHT-1,1,false); + oled.drawLine(0,1,0,HEIGHT-2,1,true); + oled.drawLine(WIDTH-1,1,WIDTH-1,HEIGHT-2,1,false); + + oled.drawLine(1,1,WIDTH-2,HEIGHT-2,1,false); + oled.drawLine(WIDTH-2,2,2,HEIGHT-2,1,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/drawPageSeg.js b/tests/sh1106/drawPageSeg.js new file mode 100644 index 0000000..57c1904 --- /dev/null +++ b/tests/sh1106/drawPageSeg.js @@ -0,0 +1,45 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + oled.clearDisplay(true); + + var p = 0, s = 0; + oled.drawPageSeg(p, s, 0xFF, 1, true); + setInterval(() => { + oled.drawPageSeg(p, s, 0x00, false); + if (s + 1 >= WIDTH) { + p = (p + 1) % 8; + } + s = (s + 1) % WIDTH; + oled.drawPageSeg(p, s, 0xFF, 1, true); + }, 10); + +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/drawPixel.js b/tests/sh1106/drawPixel.js new file mode 100644 index 0000000..b246153 --- /dev/null +++ b/tests/sh1106/drawPixel.js @@ -0,0 +1,55 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.drawPixel([ + [0, 0, 1], + [WIDTH - 1, 0, 1], + [0, HEIGHT - 1, 1], + [WIDTH - 1, HEIGHT - 1, 1] + ], true); + setTimeout(()=>{ + oled.clearDisplay(true); + },4000); + setTimeout(() => { + var x = 0, y = 0; + oled.drawPixel([x, y, 1], true); + setInterval(() => { + oled.drawPixel([x, y, 0], false); + if ((x + 1) % WIDTH == 0) { + y = (y + 1) % HEIGHT; + } + x = (x + 1) % WIDTH; + oled.drawPixel([x, y, 1], true); + }, 10); + }, 5000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/drawRGBAImage.js b/tests/sh1106/drawRGBAImage.js new file mode 100644 index 0000000..e8f3223 --- /dev/null +++ b/tests/sh1106/drawRGBAImage.js @@ -0,0 +1,56 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const path = require('path'); +const dirresources = path.join(__dirname,"../..","resources/"); +var fs = require('fs'); +var PNG = require('pngjs').PNG; + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const piLogo = "rpi-frambuesa.png"; + var image; + if (typeof piLogo === 'string' && !piLogo.includes("/")) { + image = dirresources + piLogo; + } else { + console.log("Invalid image filename"); + process.exit(1); + } + if (!fs.statSync(image).isFile()) { + console.log("file " + image + "not exist."); + process.exit(1); + } + fs.createReadStream(image) + .pipe(new PNG({ filterType: 4 })) + .on('parsed', function () { + oled.drawRGBAImage( + this, + Math.floor((WIDTH - this.width) / 2), //x-pos center width + Math.floor((HEIGHT - this.height) / 2),//y-pos center height + true + ); + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/sh1106/fillRect.js b/tests/sh1106/fillRect.js new file mode 100644 index 0000000..9c72309 --- /dev/null +++ b/tests/sh1106/fillRect.js @@ -0,0 +1,41 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.fillRect(0,0,7,5,1,false); + oled.fillRect(WIDTH-7,0,7,5,1,false); + oled.fillRect(0,HEIGHT-5,7,5,1,false); + oled.fillRect(WIDTH-7,HEIGHT-5,7,5,1,false); + + oled.fillRect(7,5,114,54,1,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/image.js b/tests/sh1106/image.js new file mode 100644 index 0000000..9448f3a --- /dev/null +++ b/tests/sh1106/image.js @@ -0,0 +1,36 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const font = require('oled-font-pack'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.image(0,0,"WaterBrain.png",font.oled_5x7,false,false,true,false); + setTimeout(()=>{ + oled.image(0,0,"",font.oled_5x7,true,true,false,false); + oled.image(30,3,"rpi-frambuesa.png",font.oled_5x7,true,false,false,false); + },5000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/sh1106/package-lock.json b/tests/sh1106/package-lock.json new file mode 100644 index 0000000..4e50139 --- /dev/null +++ b/tests/sh1106/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "examples", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "examples", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "oled-font-pack": "^1.0.1", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "node_modules/i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/oled-font-pack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/oled-font-pack/-/oled-font-pack-1.0.1.tgz", + "integrity": "sha512-vu10Fx63RJfKXcuYdgd9/6T0+u8D4GW3NP4yTJiI4pe/GV384p+O3sLxfpEYIszZjrtBxEZh/u8xXU7S9FMH6w==" + }, + "node_modules/png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "dependencies": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "node_modules/temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "dependencies": { + "es6-shim": "latest" + }, + "engines": { + "node": ">=0.8.0" + } + } + }, + "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "requires": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + } + }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "oled-font-pack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/oled-font-pack/-/oled-font-pack-1.0.1.tgz", + "integrity": "sha512-vu10Fx63RJfKXcuYdgd9/6T0+u8D4GW3NP4yTJiI4pe/GV384p+O3sLxfpEYIszZjrtBxEZh/u8xXU7S9FMH6w==" + }, + "png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "requires": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, + "pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "requires": { + "es6-shim": "latest" + } + } + } +} diff --git a/tests/sh1106/package.json b/tests/sh1106/package.json new file mode 100644 index 0000000..ce15f6a --- /dev/null +++ b/tests/sh1106/package.json @@ -0,0 +1,27 @@ +{ + "name": "examples", + "version": "1.0.0", + "description": "Testing OLED SSD1306 display with nodeJS", + "main": "clock.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/haraldkubota/oled-i2c-bus" + }, + "keywords": [ + "oled", + "i2c" + ], + "author": "Harald Kubota", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "oled-font-pack": "^1.0.1", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } +} diff --git a/tests/sh1106/update.js b/tests/sh1106/update.js new file mode 100644 index 0000000..db5a530 --- /dev/null +++ b/tests/sh1106/update.js @@ -0,0 +1,47 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + oled.clearDisplay(true); + + var p = 0, s = 0; + oled.drawPageSeg(p, s, 0xFF, 1,false); + oled.update(); + setInterval(() => { + oled.drawPageSeg(p, s, 0x00, false); + if (s + 1 >= WIDTH) { + p = (p + 1) % 8; + } + s = (s + 1) % WIDTH; + oled.drawPageSeg(p, s, 0xFF, 1,false); + oled.update(); + }, 10); + +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/sh1106/wifi.js b/tests/sh1106/wifi.js new file mode 100644 index 0000000..b9be6c1 --- /dev/null +++ b/tests/sh1106/wifi.js @@ -0,0 +1,40 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +var percentage = 0; +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.wifi(5,4,percentage); + setInterval(update, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + +function update() { + percentage = (percentage+20)%100; + oled.wifi(5,4,percentage); + } + + + diff --git a/tests/sh1106/writeString.js b/tests/sh1106/writeString.js new file mode 100644 index 0000000..f0eaf8e --- /dev/null +++ b/tests/sh1106/writeString.js @@ -0,0 +1,47 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const font = require('oled-font-pack'); + + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C, + bus: 3, + driver: 'SH1106' +}; + +try { + const i2cBus = i2c.openSync(opts.bus); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.setCursor(10,0); + oled.writeString(font.oled_5x7,1,"Hello",1,false,false); + oled.setCursor(10,10); + oled.writeString(font.oled_5x7,2,"Hello",1,false,false); + oled.setCursor(10,40); + oled.writeString(font.oled_3x5,1,"Hello",1,false,false); + oled.setCursor(10,50); + oled.writeString(font.oled_3x5,2,"Hello",1,false,false); + oled.setCursor(70,40); + oled.writeString(font.oled_3x5,3,"Hello",1,false,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/battery.js b/tests/ssd1306/battery.js new file mode 100644 index 0000000..a2e6101 --- /dev/null +++ b/tests/ssd1306/battery.js @@ -0,0 +1,38 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +var percentage = 0; +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.battery(5,4,percentage); + setInterval(update, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + +function update() { + percentage = (percentage+20)%100; + oled.battery(5,4,percentage); + } + + + diff --git a/tests/ssd1306/bluetooth.js b/tests/ssd1306/bluetooth.js new file mode 100644 index 0000000..a02c119 --- /dev/null +++ b/tests/ssd1306/bluetooth.js @@ -0,0 +1,32 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.bluetooth(5,4); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + diff --git a/tests/ssd1306/control.js b/tests/ssd1306/control.js new file mode 100644 index 0000000..ee649ca --- /dev/null +++ b/tests/ssd1306/control.js @@ -0,0 +1,82 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const path = require("path"); +const dirresources = path.join(__dirname,"../..","resources/"); +var fs = require('fs'); +var PNG = require('pngjs').PNG; + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const piLogo = "rpi-frambuesa.png"; + var image; + if (typeof piLogo === 'string' && !piLogo.includes("/")) { + image = dirresources + piLogo; + } else { + console.log("Invalid image filename"); + process.exit(1); + } + if (!fs.statSync(image).isFile()) { + console.log("file " + image + "not exist."); + process.exit(1); + } + fs.createReadStream(image) + .pipe(new PNG({ filterType: 4 })) + .on('parsed', function () { + oled.drawRGBAImage( + this, + Math.floor((WIDTH - this.width) / 2), //x-pos center width + Math.floor((HEIGHT - this.height) / 2),//y-pos center height + true + ); + setTimeout(()=>{ + oled.turnOffDisplay(); + },2000) + setTimeout(()=>{ + oled.turnOnDisplay(); + },3000) + setTimeout(()=>{ + oled.dimDisplay(true); + },4000) + setTimeout(()=>{ + oled.dimDisplay(false); + },5000) + setTimeout(()=>{ + oled.invertDisplay(true); + },6000) + setTimeout(()=>{ + oled.invertDisplay(false); + },7000); + setTimeout(()=>{ + oled.startScroll("right",0,127); + },8000); + setTimeout(()=>{ + oled.stopScroll(); + },17000) + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/drawBitmap.js b/tests/ssd1306/drawBitmap.js new file mode 100644 index 0000000..f6e9854 --- /dev/null +++ b/tests/ssd1306/drawBitmap.js @@ -0,0 +1,37 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const pngparse = require('pngparse'); +const path = require('path'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const image_file = path.join(__dirname,'../../resources','icon_128x64_kiss.png'); + pngparse.parseFile(image_file, function(err, image) { + if(err){ + process.exit(1); + } + oled.drawBitmap(image.data,true); + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/ssd1306/drawLine.js b/tests/ssd1306/drawLine.js new file mode 100644 index 0000000..88c90a2 --- /dev/null +++ b/tests/ssd1306/drawLine.js @@ -0,0 +1,40 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.drawLine(0,0,WIDTH-1,0,1,false); + oled.drawLine(0,HEIGHT-1,WIDTH-1,HEIGHT-1,1,false); + oled.drawLine(0,1,0,HEIGHT-2,1,true); + oled.drawLine(WIDTH-1,1,WIDTH-1,HEIGHT-2,1,false); + + oled.drawLine(1,1,WIDTH-2,HEIGHT-2,1,false); + oled.drawLine(WIDTH-2,2,2,HEIGHT-2,1,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/drawPageSeg.js b/tests/ssd1306/drawPageSeg.js new file mode 100644 index 0000000..d8e3c5e --- /dev/null +++ b/tests/ssd1306/drawPageSeg.js @@ -0,0 +1,43 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + var p=0,s=0; + oled.drawPageSeg(p,s,0xFF,1,true); + setInterval(()=>{ + oled.drawPageSeg(p,s,0x00,false); + if(s+1 >= WIDTH){ + p = (p+1)%8; + } + s = (s+1)%WIDTH; + oled.drawPageSeg(p,s,0xFF,1,true); + },10); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/drawPixel.js b/tests/ssd1306/drawPixel.js new file mode 100644 index 0000000..740e68e --- /dev/null +++ b/tests/ssd1306/drawPixel.js @@ -0,0 +1,53 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.drawPixel([ + [0, 0, 1], + [WIDTH - 1, 0, 1], + [0, HEIGHT - 1, 1], + [WIDTH - 1, HEIGHT - 1, 1] + ], true); + setTimeout(()=>{ + oled.clearDisplay(true); + },4000); + setTimeout(() => { + var x = 0, y = 0; + oled.drawPixel([x, y, 1], true); + setInterval(() => { + oled.drawPixel([x, y, 0], false); + if ((x + 1) % WIDTH == 0) { + y = (y + 1) % HEIGHT; + } + x = (x + 1) % WIDTH; + oled.drawPixel([x, y, 1], true); + }, 10); + }, 5000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/drawRGBAImage.js b/tests/ssd1306/drawRGBAImage.js new file mode 100644 index 0000000..a80d3b3 --- /dev/null +++ b/tests/ssd1306/drawRGBAImage.js @@ -0,0 +1,54 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const path = require('path'); +const dirresources = path.join(__dirname,"../..", "resources/"); +var fs = require('fs'); +var PNG = require('pngjs').PNG; + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + const piLogo = "rpi-frambuesa.png"; + var image; + if (typeof piLogo === 'string' && !piLogo.includes("/")) { + image = dirresources + piLogo; + } else { + console.log("Invalid image filename"); + process.exit(1); + } + if (!fs.statSync(image).isFile()) { + console.log("file " + image + "not exist."); + process.exit(1); + } + fs.createReadStream(image) + .pipe(new PNG({ filterType: 4 })) + .on('parsed', function () { + oled.drawRGBAImage( + this, + Math.floor((WIDTH - this.width) / 2), //x-pos center width + Math.floor((HEIGHT - this.height) / 2),//y-pos center height + true + ); + }); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/ssd1306/fillRect.js b/tests/ssd1306/fillRect.js new file mode 100644 index 0000000..a23d559 --- /dev/null +++ b/tests/ssd1306/fillRect.js @@ -0,0 +1,39 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.fillRect(0,0,7,5,1,false); + oled.fillRect(WIDTH-7,0,7,5,1,false); + oled.fillRect(0,HEIGHT-5,7,5,1,false); + oled.fillRect(WIDTH-7,HEIGHT-5,7,5,1,false); + + oled.fillRect(7,5,114,54,1,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + + diff --git a/tests/ssd1306/image.js b/tests/ssd1306/image.js new file mode 100644 index 0000000..240d078 --- /dev/null +++ b/tests/ssd1306/image.js @@ -0,0 +1,34 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const font = require('oled-font-pack'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.image(0,0,"WaterBrain.png",font.oled_5x7,false,false,true,false); + setTimeout(()=>{ + oled.image(0,0,"",font.oled_5x7,true,true,false,false); + oled.image(30,3,"rpi-frambuesa.png",font.oled_5x7,true,false,false,false); + },5000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + diff --git a/tests/ssd1306/package-lock.json b/tests/ssd1306/package-lock.json new file mode 100644 index 0000000..f1aa85e --- /dev/null +++ b/tests/ssd1306/package-lock.json @@ -0,0 +1,182 @@ +{ + "name": "examples", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "examples", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "oled-font-3x5": "^1.0.0", + "oled-font-5x7": "~1.0.0", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "node_modules/i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "node_modules/oled-font-3x5": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oled-font-3x5/-/oled-font-3x5-1.0.0.tgz", + "integrity": "sha512-/lxmbxy52EwHEYinSly1D+7kMVMKrWOZ9Uy5GUeRMwA8Clvx7ocNazZ62ZQkhIKOrR/nmNDHxY80ip7Ra/MdpA==" + }, + "node_modules/oled-font-5x7": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/oled-font-5x7/-/oled-font-5x7-1.0.3.tgz", + "integrity": "sha512-l25WvKft8CgXYxtaqKdYrAS1P91rnUUUIiOXojAOvjNCsfFzIl1aEsE2JuaRgMh1Euo7slm5lX0w+1qNkL8PpQ==" + }, + "node_modules/png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "dependencies": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "node_modules/temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "dependencies": { + "es6-shim": "latest" + }, + "engines": { + "node": ">=0.8.0" + } + } + }, + "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "es6-shim": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "floyd-steinberg": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/floyd-steinberg/-/floyd-steinberg-1.0.6.tgz", + "integrity": "sha512-gzlre+taSQzEY+nCusbHJFQ3zHXBkRAX4fe3szssPY/N/t1ClBX9hnHZM4o8ZvpdqLLUPEavuZ/Noj71ZaA8+A==" + }, + "i2c-bus": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/i2c-bus/-/i2c-bus-5.2.2.tgz", + "integrity": "sha512-b2y/et08rqggEjqod6QxV0Ng+RlSkZXsdE+E1aZEQBahepZ9uFD9XfA77Y8O9SgyhwfGEn+CNxsw0gvXW1/m4g==", + "requires": { + "bindings": "^1.5.0", + "nan": "^2.14.2" + } + }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, + "oled-font-3x5": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oled-font-3x5/-/oled-font-3x5-1.0.0.tgz", + "integrity": "sha512-/lxmbxy52EwHEYinSly1D+7kMVMKrWOZ9Uy5GUeRMwA8Clvx7ocNazZ62ZQkhIKOrR/nmNDHxY80ip7Ra/MdpA==" + }, + "oled-font-5x7": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/oled-font-5x7/-/oled-font-5x7-1.0.3.tgz", + "integrity": "sha512-l25WvKft8CgXYxtaqKdYrAS1P91rnUUUIiOXojAOvjNCsfFzIl1aEsE2JuaRgMh1Euo7slm5lX0w+1qNkL8PpQ==" + }, + "png-to-lcd": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/png-to-lcd/-/png-to-lcd-1.0.3.tgz", + "integrity": "sha512-y4X4mRvZUoMv1ruQimuXixC72HfPyPZHCxlSiQkwVjBAdYlQSnkp1N3EZkgVoS+CngJdTGGW9nMw9VBkfSH39Q==", + "requires": { + "floyd-steinberg": "~1.0.4", + "pngparse": "~2.0.1" + } + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, + "pngparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", + "integrity": "sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==" + }, + "temporal": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/temporal/-/temporal-0.3.8.tgz", + "integrity": "sha512-Oifg/Jy1FqoxgAHhfrwjnO9PKrqv9JYR/KgsmsMKjpPaYWdJmzWnCVhSFAxv7ygdYILj6Kd+v4YQtaxF0ZCjGA==", + "requires": { + "es6-shim": "latest" + } + } + } +} diff --git a/tests/ssd1306/package.json b/tests/ssd1306/package.json new file mode 100644 index 0000000..ce15f6a --- /dev/null +++ b/tests/ssd1306/package.json @@ -0,0 +1,27 @@ +{ + "name": "examples", + "version": "1.0.0", + "description": "Testing OLED SSD1306 display with nodeJS", + "main": "clock.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/haraldkubota/oled-i2c-bus" + }, + "keywords": [ + "oled", + "i2c" + ], + "author": "Harald Kubota", + "license": "MIT", + "dependencies": { + "i2c-bus": "^5.2.2", + "oled-font-pack": "^1.0.1", + "png-to-lcd": "~1.0.2", + "pngjs": "^3.3.3", + "pngparse": "~2.0.1", + "temporal": "^0.3.8" + } +} diff --git a/tests/ssd1306/wifi.js b/tests/ssd1306/wifi.js new file mode 100644 index 0000000..d24bc88 --- /dev/null +++ b/tests/ssd1306/wifi.js @@ -0,0 +1,38 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +var percentage = 0; +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + oled.wifi(5,4,percentage); + setInterval(update, 1000); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + +function update() { + percentage = (percentage+20)%100; + oled.wifi(5,4,percentage); + } + + + diff --git a/tests/ssd1306/writeString.js b/tests/ssd1306/writeString.js new file mode 100644 index 0000000..12c111a --- /dev/null +++ b/tests/ssd1306/writeString.js @@ -0,0 +1,44 @@ +"use strict"; + +// NOTE: On newer versions of Raspberry Pi the I2C is set to 1 for hardware I2C and 3 for software I2C, +// however on other platforms you may need to adjust if to +// another value, for example 0. +const i2c = require('i2c-bus'); +const display = require('../../oled'); +const font= require('oled-font-pack'); + +const HEIGHT = 64; +const WIDTH = 128; +var opts = { + width: WIDTH, + height: HEIGHT, + address: 0x3C +}; + +try { + const i2cBus = i2c.openSync(opts.bus || 1); + var oled = new display(i2cBus, opts); + + oled.clearDisplay(true); + + oled.setCursor(10,0); + oled.writeString(font.oled_5x7,1,"Hello",1,false,false); + oled.setCursor(10,10); + oled.writeString(font.oled_5x7,2,"Hello",1,false,false); + oled.setCursor(10,40); + oled.writeString(font.oled_3x5,1,"Hello",1,false,false); + oled.setCursor(10,50); + oled.writeString(font.oled_3x5,2,"Hello",1,false,false); + oled.setCursor(70,40); + oled.writeString(font.oled_3x5,3,"Hello",1,false,true); +} +catch (err) { + // Print an error message and terminate the application + console.log(err.message); + process.exit(1); +} + + + + +