import { Board , Pin } from "johnny-five" ; enum Protocol { I2C , SPI } enum TransferType { Command , Data } type Direction = 'left' | 'left diagonal' | 'right' | 'right diagonal' ; type Black = 0x00 ; type White = 0x01 | 0xff ; type Color = Black | White ; type Pixel = [ number , number , Color ] ; interface OledOptions { height ? : number ; width ? : number ; address ? : number ; microview ? : boolean ; secondaryPin ? : number ; resetPin ? : number ; data ? : number ; command ? : number ; } interface Font { monospace : boolean ; width : number ; height : number ; fontData : number [ ] ; lookup : string [ ] ; } interface ScreenConfig { multiplex : number ; compins : number ; coloffset : number ; } interface SPIConfig { dcPin : number ; ssPin : number ; rstPin : number ; clkPin : number ; mosiPin : number ; } export = class Oled { private readonly HEIGHT : number ; private readonly WIDTH : number ; private readonly ADDRESS : number ; private readonly PROTOCOL : Protocol ; private readonly MICROVIEW : boolean ; private readonly SECONDARYPIN : number ; private readonly RESETPIN : number ; private readonly DATA : number ; private readonly COMMAND : number ; private readonly board : Board ; private readonly five : any ; private readonly screenConfig : ScreenConfig ; private readonly SPIconfig : SPIConfig ; private dcPin : Pin ; private ssPin : Pin ; private clkPin : Pin ; private mosiPin : Pin ; private rstPin : Pin ; private static readonly DISPLAY_OFF : number = 0xAE ; private static readonly DISPLAY_ON : number = 0xAF ; private static readonly SET_DISPLAY_CLOCK_DIV : number = 0xD5 ; private static readonly SET_MULTIPLEX : number = 0xA8 ; private static readonly SET_DISPLAY_OFFSET : number = 0xD3 ; private static readonly SET_START_LINE : number = 0x00 ; private static readonly CHARGE_PUMP : number = 0x8D ; private static readonly EXTERNAL_VCC : boolean = false ; private static readonly MEMORY_MODE : number = 0x20 ; private static readonly SEG_REMAP : number = 0xA1 ; private static readonly COM_SCAN_DEC : number = 0xC8 ; private static readonly COM_SCAN_INC : number = 0xC0 ; private static readonly SET_COM_PINS : number = 0xDA ; private static readonly SET_CONTRAST : number = 0x81 ; private static readonly SET_PRECHARGE : number = 0xd9 ; private static readonly SET_VCOM_DETECT : number = 0xDB ; private static readonly DISPLAY_ALL_ON_RESUME : number = 0xA4 ; private static readonly NORMAL_DISPLAY : number = 0xA6 ; private static readonly COLUMN_ADDR : number = 0x21 ; private static readonly PAGE_ADDR : number = 0x22 ; private static readonly INVERT_DISPLAY : number = 0xA7 ; private static readonly ACTIVATE_SCROLL : number = 0x2F ; private static readonly DEACTIVATE_SCROLL : number = 0x2E ; private static readonly SET_VERTICAL_SCROLL_AREA : number = 0xA3 ; private static readonly RIGHT_HORIZONTAL_SCROLL : number = 0x26 ; private static readonly LEFT_HORIZONTAL_SCROLL : number = 0x27 ; private static readonly VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL : number = 0x29 ; private static readonly VERTICAL_AND_LEFT_HORIZONTAL_SCROLL : number = 0x2A ; private buffer : Buffer ; private cursor_x : number ; private cursor_y : number ; private dirtyBytes : number [ ] ; public constructor ( board , five , opts ) { this . HEIGHT = opts . height || 32 ; this . WIDTH = opts . width || 128 ; this . ADDRESS = opts . address || 0x3C ; this . PROTOCOL = ( opts . address ) ? Protocol . I2C : Protocol . SPI ; this . MICROVIEW = opts . microview || false ; this . SECONDARYPIN = opts . secondaryPin || 12 ; this . RESETPIN = opts . resetPin || 4 ; this . DATA = opts . data || 0x40 ; this . COMMAND = opts . command || 0x00 ; this . cursor_x = 0 ; this . cursor_y = 0 ; this . buffer = Buffer . alloc ( ( this . WIDTH * this . HEIGHT ) / 8 ) ; this . buffer . fill ( 0x00 ) ; this . dirtyBytes = [ ] ; this . board = board ; this . five = five ; const config = { '128x32' : { 'multiplex' : 0x1F , 'compins' : 0x02 , 'coloffset' : 0 } , '128x64' : { 'multiplex' : 0x3F , 'compins' : 0x12 , 'coloffset' : 0 } , '96x16' : { 'multiplex' : 0x0F , 'compins' : 0x2 , 'coloffset' : 0 } , '64x48' : { 'multiplex' : 0x2F , 'compins' : 0x12 , 'coloffset' : ( this . MICROVIEW ) ? 32 : 0 } } ; if ( this . MICROVIEW ) { this . SPIconfig = { 'dcPin' : 8 , 'ssPin' : 10 , 'rstPin' : 7 , 'clkPin' : 13 , 'mosiPin' : 11 } ; } else if ( this . PROTOCOL === Protocol . SPI ) { this . SPIconfig = { 'dcPin' : 11 , 'ssPin' : this . SECONDARYPIN , 'rstPin' : 13 , 'clkPin' : 10 , 'mosiPin' : 9 } ; } const screenSize = `${ this . WIDTH }x${ this . HEIGHT }` ; this . screenConfig = config [ screenSize ] ; if ( this . PROTOCOL === Protocol . I2C ) { this . _setUpI2C ( opts ) ; } else { this . _setUpSPI ( ) ; } this . _initialise ( ) ; } private _initialise ( ) { const initSeq = [ Oled . DISPLAY_OFF , Oled . SET_DISPLAY_CLOCK_DIV , 0x80 , Oled . SET_MULTIPLEX , this . screenConfig . multiplex , Oled . SET_DISPLAY_OFFSET , 0x00 , Oled . SET_START_LINE , Oled . CHARGE_PUMP , 0x14 , Oled . MEMORY_MODE , 0x00 , Oled . SEG_REMAP , Oled . COM_SCAN_DEC , Oled . SET_COM_PINS , this . screenConfig . compins , Oled . SET_CONTRAST , 0x8F , Oled . SET_PRECHARGE , 0xF1 , Oled . SET_VCOM_DETECT , 0x40 , Oled . DISPLAY_ALL_ON_RESUME , Oled . NORMAL_DISPLAY , Oled . DISPLAY_ON ] ; for ( let i = 0 ; i < initSeq . length ; i ++ ) { this . _transfer ( TransferType . Command , initSeq [ i ] ) ; } } private _setUpSPI ( ) { this . dcPin = new this . five . Pin ( this . SPIconfig . dcPin ) ; this . ssPin = new this . five . Pin ( this . SPIconfig . ssPin ) ; this . clkPin = new this . five . Pin ( this . SPIconfig . clkPin ) ; this . mosiPin = new this . five . Pin ( this . SPIconfig . mosiPin ) ; this . rstPin = new this . five . Pin ( this . SPIconfig . rstPin ) ; this . rstPin . low ( ) ; this . rstPin . high ( ) ; this . ssPin . high ( ) ; } private _setUpI2C ( opts ) { this . board . io . i2cConfig ( opts ) ; this . rstPin = new this . five . Pin ( this . RESETPIN ) ; this . rstPin . low ( ) ; this . rstPin . high ( ) ; } private _transfer ( type , val ) { let control ; if ( type === TransferType . Data ) { control = this . DATA ; } else if ( type === TransferType . Command ) { control = this . COMMAND ; } else { return ; } if ( this . PROTOCOL === Protocol . I2C ) { this . board . io . i2cWrite ( this . ADDRESS , [ control , val ] ) ; } else { this . _writeSPI ( val , type ) ; } } private _writeSPI ( byte , mode ) { if ( mode === TransferType . Command ) { this . dcPin . low ( ) ; } else { this . dcPin . high ( ) ; } this . ssPin . low ( ) ; for ( let bit = 7 ; bit >= 0 ; bit -- ) { this . clkPin . low ( ) ; if ( byte & ( 1 << bit ) ) { this . mosiPin . high ( ) ; } else { this . mosiPin . low ( ) ; } this . clkPin . high ( ) ; } this . ssPin . high ( ) ; } private _readI2C ( fn ) { this . board . io . i2cReadOnce ( this . ADDRESS , 1 , ( data ) => { fn ( data ) ; } ) ; } private _waitUntilReady ( callback ) { const oled = this ; const tick = ( callback ) => { oled . _readI2C ( ( byte ) => { const busy = byte >> 7 & 1 ; if ( ! busy ) { callback ( ) ; } else { console . log ( 'I\'m busy!' ) ; setTimeout ( tick , 0 ) ; } } ) ; } ; if ( this . PROTOCOL === Protocol . I2C ) { setTimeout ( ( ) => { tick ( callback ) ; } , 0 ) ; } else { callback ( ) ; } } public setCursor ( x , y ) { this . cursor_x = x ; this . cursor_y = y ; } private _invertColor ( color ) { return ( color === 0 ) ? 1 : 0 ; } public writeString ( font , size , string , color , wrap , linespacing , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; const wordArr = string . split ( ' ' ) ; const len = wordArr . length ; let offset = this . cursor_x ; let padding = 0 ; const letspace = 1 ; const leading = linespacing || 2 ; for ( let i = 0 ; i < len ; i += 1 ) { if ( i < len - 1 ) wordArr [ i ] += ' ' ; const stringArr = wordArr [ i ] . split ( '' ) ; const slen = stringArr . length ; const compare = ( font . width * size * slen ) + ( size * ( len - 1 ) ) ; if ( wrap && len > 1 && ( offset >= ( this . WIDTH - compare ) ) ) { offset = 1 ; this . cursor_y += ( font . height * size ) + size + leading ; this . setCursor ( offset , this . cursor_y ) ; } for ( let i = 0 ; i < slen ; i += 1 ) { const charBuf = this . _findCharBuf ( font , stringArr [ i ] ) ; const charBytes = this . _readCharBytes ( charBuf ) ; this . _drawChar ( font , charBytes , size , color , false ) ; this . fillRect ( offset - padding , this . cursor_y , padding , ( font . height * size ) , this . _invertColor ( color ) , false ) ; padding = ( stringArr [ i ] === ' ' ) ? 0 : size + letspace ; offset += ( font . width * size ) + padding ; if ( wrap && ( offset >= ( this . WIDTH - font . width - letspace ) ) ) { offset = 1 ; this . cursor_y += ( font . height * size ) + size + leading ; } this . setCursor ( offset , this . cursor_y ) ; } } if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } private _drawChar ( font , byteArray , size , color , sync ? ) { const x = this . cursor_x ; const y = this . cursor_y ; let c = 0 ; let pagePos = 0 ; for ( let i = 0 ; i < byteArray . length ; i += 1 ) { pagePos = Math . floor ( i / font . width ) * 8 ; for ( let j = 0 ; j < 8 ; j += 1 ) { const pixelState = ( byteArray [ i ] [ j ] === 1 ) ? color : this . _invertColor ( color ) ; let xpos ; let ypos ; if ( size === 1 ) { xpos = x + c ; ypos = y + j + pagePos ; this . drawPixel ( [ xpos , ypos , pixelState ] , false ) ; } else { xpos = x + ( i * size ) ; ypos = y + ( j * size ) ; this . fillRect ( xpos , ypos , size , size , pixelState , false ) ; } } c = ( c < font . width - 1 ) ? c += 1 : 0 ; } } private _readCharBytes ( byteArray ) { let bitArr = [ ] ; const bitCharArr = [ ] ; for ( let i = 0 ; i < byteArray . length ; i += 1 ) { const byte = byteArray [ i ] ; for ( let j = 0 ; j < 8 ; j += 1 ) { const bit = byte >> j & 1 ; bitArr . push ( bit ) ; } bitCharArr . push ( bitArr ) ; bitArr = [ ] ; } return bitCharArr ; } private _findCharBuf ( font , c ) { const charLength = Math . ceil ( ( font . width * font . height ) / 8 ) ; const cBufPos = font . lookup . indexOf ( c ) * charLength ; return font . fontData . slice ( cBufPos , cBufPos + charLength ) ; } public update ( ) { this . _waitUntilReady ( ( ) => { const displaySeq = [ Oled . COLUMN_ADDR , this . screenConfig . coloffset , this . screenConfig . coloffset + this . WIDTH - 1 , Oled . PAGE_ADDR , 0 , ( this . HEIGHT / 8 ) - 1 ] ; const displaySeqLen = displaySeq . length ; const bufferLen = this . buffer . length ; for ( let i = 0 ; i < displaySeqLen ; i += 1 ) { this . _transfer ( TransferType . Command , displaySeq [ i ] ) ; } for ( let i = 0 ; i < bufferLen ; i += 1 ) { this . _transfer ( TransferType . Data , this . buffer [ i ] ) ; } } ) ; this . dirtyBytes = [ ] ; } public dimDisplay ( bool ) { let contrast ; if ( bool ) { contrast = 0 ; } else { contrast = 0xCF ; } this . _transfer ( TransferType . Command , Oled . SET_CONTRAST ) ; this . _transfer ( TransferType . Command , contrast ) ; } public turnOffDisplay ( ) { this . _transfer ( TransferType . Command , Oled . DISPLAY_OFF ) ; } public turnOnDisplay ( ) { this . _transfer ( TransferType . Command , Oled . DISPLAY_ON ) ; } public clearDisplay ( sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : 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 ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } public invertDisplay ( bool ) { if ( bool ) { this . _transfer ( TransferType . Command , Oled . INVERT_DISPLAY ) ; } else { this . _transfer ( TransferType . Command , Oled . NORMAL_DISPLAY ) ; } } public drawBitmap ( pixels , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; for ( let i = 0 ; i < pixels . length ; i ++ ) { const x = Math . floor ( i % this . WIDTH ) ; const y = Math . floor ( i / this . WIDTH ) ; this . drawPixel ( [ x , y , pixels [ i ] ] , false ) ; } if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } private _isSinglePixel ( pixels ) { return typeof pixels [ 0 ] !== 'object' ; } public drawPixel ( pixels , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; if ( this . _isSinglePixel ( pixels ) ) pixels = [ pixels ] ; pixels . forEach ( ( el ) => { const [ x , y , color ] = el ; if ( x > this . WIDTH || y > this . HEIGHT ) return ; let byte = 0 ; const page = Math . floor ( y / 8 ) ; const pageShift = 0x01 << ( y - 8 * page ) ; ( page === 0 ) ? byte = x : byte = x + ( this . WIDTH * page ) ; if ( color === 0 ) { this . buffer [ byte ] &= ~ pageShift ; } else { this . buffer [ byte ] |= pageShift ; } if ( this . dirtyBytes . indexOf ( byte ) === - 1 ) { this . dirtyBytes . push ( byte ) ; } } , this ) ; if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } private _updateDirtyBytes ( byteArray ) { const blen = byteArray . length ; this . _waitUntilReady ( ( ) => { let pageStart = Infinity ; let pageEnd = 0 ; let colStart = Infinity ; let colEnd = 0 ; let any = false ; for ( let i = 0 ; i < blen ; i += 1 ) { const b = byteArray [ i ] ; if ( ( b >= 0 ) && ( b < this . buffer . length ) ) { const page = b / this . WIDTH | 0 ; if ( page < pageStart ) pageStart = page ; if ( page > pageEnd ) pageEnd = page ; const col = b % this . WIDTH ; if ( col < colStart ) colStart = col ; if ( col > colEnd ) colEnd = col ; any = true ; } } if ( ! any ) return ; const displaySeq = [ Oled . COLUMN_ADDR , colStart , colEnd , Oled . PAGE_ADDR , pageStart , pageEnd ] ; const displaySeqLen = displaySeq . length ; for ( let i = 0 ; i < displaySeqLen ; i += 1 ) { this . _transfer ( TransferType . Command , displaySeq [ i ] ) ; } for ( let i = pageStart ; i <= pageEnd ; i += 1 ) { for ( let j = colStart ; j <= colEnd ; j += 1 ) { this . _transfer ( TransferType . Data , this . buffer [ this . WIDTH * i + j ] ) ; } } } ) ; this . dirtyBytes = [ ] ; } public drawLine ( x0 , y0 , x1 , y1 , color , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; const dx = Math . abs ( x1 - x0 ) ; const sx = x0 < x1 ? 1 : - 1 ; const dy = Math . abs ( y1 - y0 ) ; const sy = y0 < y1 ? 1 : - 1 ; let err = ( dx > dy ? dx : - dy ) / 2 ; while ( true ) { this . drawPixel ( [ x0 , y0 , color ] , false ) ; if ( x0 === x1 && y0 === y1 ) break ; const e2 = err ; if ( e2 > - dx ) { err -= dy ; x0 += sx ; } if ( e2 < dy ) { err += dx ; y0 += sy ; } } if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } public drawRect ( x , y , w , h , color , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; this . drawLine ( x , y , x + w , y , color , false ) ; this . drawLine ( x , y + 1 , x , y + h - 1 , color , false ) ; this . drawLine ( x + w , y + 1 , x + w , y + h - 1 , color , false ) ; this . drawLine ( x , y + h - 1 , x + w , y + h - 1 , color , false ) ; if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } ; public fillRect ( x , y , w , h , color , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; for ( let i = x ; i < x + w ; i += 1 ) { this . drawLine ( i , y , i , y + h - 1 , color , false ) ; } if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } /**
     * Draw a circle outline
     *
     * This method is ad verbatim translation from the corresponding
     * method on the Adafruit GFX library
     * https://github.com/adafruit/Adafruit-GFX-Library
     */ public drawCircle ( x0 , y0 , r , color , sync ? ) { const immed = ( typeof sync === 'undefined' ) ? true : sync ; let f = 1 - r ; let ddF_x = 1 ; let ddF_y = - 2 * r ; let x = 0 ; let y = r ; this . drawPixel ( [ [ x0 , y0 + r , color ] , [ x0 , y0 - r , color ] , [ x0 + r , y0 , color ] , [ x0 - r , y0 , color ] ] , false ) ; while ( x < y ) { if ( f >= 0 ) { y -- ; ddF_y += 2 ; f += ddF_y ; } x ++ ; ddF_x += 2 ; f += ddF_x ; this . drawPixel ( [ [ x0 + x , y0 + y , color ] , [ x0 - x , y0 + y , color ] , [ x0 + x , y0 - y , color ] , [ x0 - x , y0 - y , color ] , [ x0 + y , y0 + x , color ] , [ x0 - y , y0 + x , color ] , [ x0 + y , y0 - x , color ] , [ x0 - y , y0 - x , color ] ] , false ) ; } if ( immed ) { this . _updateDirtyBytes ( this . dirtyBytes ) ; } } ; public startScroll ( dir , start , stop ) { const cmdSeq = [ ] ; switch ( dir ) { case 'right' : cmdSeq . push ( Oled . RIGHT_HORIZONTAL_SCROLL ) ; break ; case 'left' : cmdSeq . push ( Oled . LEFT_HORIZONTAL_SCROLL ) ; break ; case 'left diagonal' : cmdSeq . push ( Oled . SET_VERTICAL_SCROLL_AREA , 0x00 , this . HEIGHT , Oled . VERTICAL_AND_LEFT_HORIZONTAL_SCROLL , 0x00 , start , 0x00 , stop , 0x01 , Oled . ACTIVATE_SCROLL ) ; break ; case 'right diagonal' : cmdSeq . push ( Oled . SET_VERTICAL_SCROLL_AREA , 0x00 , this . HEIGHT , Oled . VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL , 0x00 , start , 0x00 , stop , 0x01 , Oled . ACTIVATE_SCROLL ) ; break ; } this . _waitUntilReady ( ( ) => { if ( dir === 'right' || dir === 'left' ) { cmdSeq . push ( 0x00 , start , 0x00 , stop , 0x00 , 0xFF , Oled . ACTIVATE_SCROLL ) ; } for ( let i = 0 ; i < cmdSeq . length ; i += 1 ) { this . _transfer ( TransferType . Command , cmdSeq [ i ] ) ; } } ) ; } public stopScroll ( ) { this . _transfer ( TransferType . Command , Oled . DEACTIVATE_SCROLL ) ; } } ;