UNPKG

oled-rpi-i2c-bus-async

Version:

Asynchronous NodeJS module for controlling oled devices on the Raspbery Pi (including the SSD1306 and SH1106 OLED screens)

359 lines (312 loc) 11.4 kB
// SSD1306 OLED driver implementation import BaseOLED from './base-oled.mjs'; import { createLogger } from '../utils/logger.mjs'; class SSD1306 extends BaseOLED { constructor(i2c, opts) { super(i2c, opts); // Create driver-specific logger this.logger = createLogger('SSD1306'); // Set logger level if provided if (opts?.logLevel) { this.logger.setLevel(opts.logLevel); } // SSD1306 specific command definitions this.SET_START_LINE = 0x00; this.CHARGE_PUMP = 0x8d; this.SEG_REMAP = 0xa1; this.COM_SCAN_DEC = 0xc8; this.COM_SCAN_INC = 0xc0; this.SET_COM_PINS = 0xda; this.COLUMN_ADDR = 0x21; this.PAGE_ADDR = 0x22; 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; const config = { '128x32': { multiplex: 0x1f, compins: 0x02, coloffset: 0, }, '128x64': { multiplex: 0x3f, compins: 0x12, coloffset: 0, }, '96x16': { multiplex: 0x0f, compins: 0x02, coloffset: 0, }, }; const screenSize = `${this.WIDTH}x${this.HEIGHT}`; this.screenConfig = config[screenSize]; // Initialize the display this._initialise(); } /* ################################################################################################## * SSD1306 specific implementations * ################################################################################################## */ // Activate scrolling for rows start through stop using batch commands startScroll = async (dir, start, stop) => { let 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; case 'right diagonal': cmdSeq.push( this.SET_VERTICAL_SCROLL_AREA, 0x00, this.VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL, this.HEIGHT ); break; } await this._waitUntilReady(); cmdSeq.push( 0x00, start, 0x00, stop, // TODO: these need to change when diagonal 0x00, 0xff, this.ACTIVATE_SCROLL ); // Send all scroll commands in a single batch instead of individually await this._transferBatch('cmd', cmdSeq); }; // Stop scrolling display contents stopScroll = async () => { await this._transfer('cmd', this.DEACTIVATE_SCROLL); }; // Send the entire framebuffer to the oled with hardware acceleration update = async () => { // Wait for oled to be ready await this._waitUntilReady(); // Set the addressing mode for optimal full screen updates await this._transferBatch('cmd', [ this.MEMORY_MODE, this.HORIZONTAL_ADDRESSING_MODE, // Horizontal is best for full updates 0xD5, // Set display clock divide ratio/oscillator frequency 0xF0 // Set higher frequency for faster refresh (per SSD1306 datasheet section 10.1.16) ]); // Set the start and end byte locations for oled display update const 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 ]; // Send intro sequence all at once instead of individually await this._transferBatch('cmd', displaySeq); // Write buffer data in one efficient transfer const bufferToSend = Buffer.concat([Buffer.from([0x40]), this.buffer]); await this.wire.i2cWrite(this.ADDRESS, bufferToSend.length, bufferToSend); }; /* ################################################################################################## * Implementation of required base methods * ################################################################################################## */ // Optimize display initialization with batch commands for better efficiency _initialise = async () => { // Create initialization sequences with named batches for better organization const initSequences = [ { name: 'initial', commands: [ this.DISPLAY_OFF, this.SET_DISPLAY_CLOCK_DIV, 0x80 // Use datasheet-recommended clock divider for best performance ] }, { name: 'multiplex', commands: [ this.SET_MULTIPLEX, this.screenConfig.multiplex, this.SET_DISPLAY_OFFSET, 0x00, this.SET_START_LINE | 0x00 // Start at line 0 ] }, { name: 'charge_pump', commands: [ this.CHARGE_PUMP, 0x14, // Enable charge pump with optimal setting this.MEMORY_MODE, this.HORIZONTAL_ADDRESSING_MODE // Default to horizontal addressing mode for efficiency ] }, { name: 'com_config', commands: [ this.SEG_REMAP, // Column address 127 is mapped to SEG0 this.COM_SCAN_DEC, // Scan from COM[N-1] to COM0 this.SET_COM_PINS, this.HEIGHT === 32 ? 0x02 : 0x12 // Optimize COM pins for display height ] }, { name: 'contrast', commands: [ this.SET_CONTRAST, this.HEIGHT === 32 ? 0x8F : 0xCF, // Optimize contrast based on display height this.SET_PRECHARGE, 0xf1 // Phase 1 = 15, Phase 2 = 1 (optimal from datasheet) ] }, { name: 'final', commands: [ this.SET_VCOM_DETECT, this.HEIGHT === 64 ? 0x20 : 0x40, // Optimize VCOM level based on display size this.DISPLAY_ALL_ON_RESUME, // Resume to RAM content display (power efficient) this.NORMAL_DISPLAY, this.DISPLAY_ON ] } ]; // Use the common base initialization method await this._baseInitialize(initSequences); }; // Optimized dirty byte updater with page grouping and batch commands _updateDirtyBytes = async (dirtyByteArray) => { try { const dirtyByteArrayLen = dirtyByteArray.length; // If there are no dirty bytes, nothing to do if (dirtyByteArrayLen === 0) { return; } // Check if full update would be more efficient - using common method if (this._shouldDoFullUpdate(dirtyByteArray)) { // More efficient to do a full update await this.update(); // Now that all bytes are synced, reset dirty state this.dirtyBytes = []; return; } // Wait for display to be ready await this._waitUntilReady(); // Set memory mode to page addressing and optimize command discharge timing // for faster partial updates (see datasheet section 10.1.18) await this._transferBatch('cmd', [ this.MEMORY_MODE, this.PAGE_ADDRESSING_MODE, // Page addressing is better for partial updates 0xD9, // Set pre-charge period command 0x22 // Faster discharge time for partial updates ]); // Group dirty bytes by page for more efficient updates - using common method const pageGroups = this._groupDirtyBytesByPage(dirtyByteArray); // Now update each page's dirty bytes for (const [page, bytes] of pageGroups.entries()) { // Sort by column for sequential access bytes.sort((a, b) => a.col - b.col); // Optimize for consecutive columns by grouping them into ranges let currentStart = null; let currentEnd = null; let currentData = []; const flushCurrentRange = async () => { if (currentStart !== null) { // Send display position commands as a batch const displaySeq = [ this.COLUMN_ADDR, currentStart, currentEnd, this.PAGE_ADDR, page, page, ]; await this._transferBatch('cmd', displaySeq); // Send all data bytes for this range in a batch await this._transferBatch('data', currentData); // Reset tracking variables currentStart = null; currentEnd = null; currentData = []; } }; // Process each byte for (let i = 0; i < bytes.length; i++) { const { col, byteIndex } = bytes[i]; // If this is a new range or not consecutive with previous column if (currentStart === null || col !== currentEnd + 1) { // Flush the current range if any await flushCurrentRange(); // Start a new range currentStart = col; } // Update the end of the range currentEnd = col; // Add data to the current batch currentData.push(this.buffer[byteIndex]); } // Flush any remaining range await flushCurrentRange(); } // Reset dirty bytes this.dirtyBytes = []; } catch (err) { this._handleError('updating dirty bytes', err); } }; // Override base methods with SSD1306-specific optimizations dimDisplay = async (bool) => { // Use the enhanced base implementation with SSD1306-specific contrast values await this._enhancedDimDisplay(bool, { brightContrast: 0xCF, // Optimal bright contrast for SSD1306 dimContrast: 0x00 // SSD1306 can handle complete dimming }); }; invertDisplay = async (bool) => { // Use the enhanced base implementation with SSD1306-specific contrast values await this._enhancedInvertDisplay(bool, { normalContrast: 0x8F, // Default contrast for normal display invertedContrast: 0x60 // Lower contrast works better for inverted SSD1306 display }); }; turnOnDisplay = async () => { // Use the enhanced base implementation with SSD1306-specific options await this._enhancedTurnOnDisplay({ contrast: 0x8F, // Include additional SSD1306-specific power-on commands additionalCommands: [ this.CHARGE_PUMP, // Set charge pump 0x14, // Enable charge pump this.SET_PRECHARGE, // Set pre-charge period 0xF1 // Phase 1 = 15, Phase 2 = 1 (optimal for fast turn-on) ] }); }; turnOffDisplay = async () => { // Use the enhanced base implementation with SSD1306-specific power-down commands await this._enhancedTurnOffDisplay({ additionalCommands: [ this.CHARGE_PUMP, // Set charge pump 0x10 // Disable charge pump to save power ] }); }; } export default SSD1306;