OLED Driver (Exercise)

A class for the OLED display
from time import sleep
import framebuf

##################################################################
class OLED :
##################################################################
    def __init__( self, i2c, i2c_adr, buf = None ):
        '''We have 2 possibilities to use this class. If we do not 
        provide a buffer (which needs to be a byte-array) then the
        constructor creates a framebuffer with the associated buffer.
        the framebuffer can then be retrieved by the application 
        with the "getFramebuffer" call and then the application can
        use the api of the framebuffer to draw things.
        The second way to use the class is by providing the buffer
        to the constructor. Then no framebuffer is created and the 
        class assumes that the user has created the framebuffer 
        himself. 
        The framebuffer itself is not needed by this class. (It is 
        only created as a convenience for the user with parameters
        which match the display). The buffer, however, is used when 
        the contents is copied into the memory of the display. The 
        corresponding method ('copyFramebuffer') has an optional 
        parameter "buf" which can be used by the user to provide 
        a buffer from the appliction. If no buffer is provided the
        buffer provided by the user in this constructor is used or
        the one which was create by the constructor.

        This architecture should be convenient for the user and in 
        addition allows the user to play with multiple framebuffers
        if he wants to.'''

        if buf:
            self.buf = buf
            self.framebuffer = None
        else:
            self.buf = bytearray( 128*16)
            #self.framebuffer = framebuf.FrameBuffer( self.buf, 64, 128, framebuf.MONO_VLSB )
            self.framebuffer = framebuf.FrameBuffer( self.buf, 128,128, framebuf.MONO_VLSB )
        self.i2c = i2c
        self.oled_adr = i2c_adr
        self.landscape = False
        self.bufr = None


    def _wr_cmd( self, cmd ):
        '''This function writes a single command byte to the display controller.
        (There are one byte and two byte commands in for the controller (see 
        datasheet. Both bytes are considered "command bytes" here. Hence this
        function can be used for both types of command bytes. 

        Try to understand from the data sheet what has to be transferred to the 
        display in order that it is interpreted as a command byte. (Hint: it will
        be two bytes in total which you will need to transfer)

        It is important to note tht the i2c.writeto function expects the python
        type 'bytearray' and NOT a series of integers. Fortunately there is a 
        function in python which we can use to convert an integer (which in python
        is an object) to a series of bytes: the function is called ".to_bytes(...)"
        and is called directly on the variable holding the integer we want to convert.
        You can look up the documentation of the function in the python documentation.
        The signature is "int.to_bytes(length=1, byteorder='big')" (with default parameters).

        Btw: the esp32 is "little endian" computer (but this is not important here.)'''

        self.i2c.writeto( ???  )

    def init(self):
        '''Initialize the display. To do this we need to write configuration 
        parameters to some registers in the controller chip. What we need to write
        depends on the design of our display. We need to study the data sheet to
        retrieve the correct values. We write the values with a sequence of commands.
        The sequence is very similar for many different controller chips on the 
        market. Hence if you get one working it is easy to adapt to another display.
        Sometimes some offsets have to be adjusted since this depends on how the 
        display is soldered to the controller chip.
        Btw: most of the parameters are correctly set after a Power On Reset (POR)
        Hence many of the commands below are strictly speaking not necessary. 

        To have this a bit readable and compact we define an array with the 
        commands and then pass this array to the loop which writes one byte 
        after the other to the controller. As you can see in the data sheet
        some commands have 2 bytes and others only one byte.'''

        commands = [ 0xae,       # Turn display off during initialisation
                                 ... ??? ...
                   ]

        for cmd in commands:
            self._wr_cmd( cmd )

        sleep(0.1)               # the data sheet wants this.


    def setLandscape( self, ls = True ):
        if ls:
            if not self.bufr:
                self.bufr = bytearray( 128*16)
            self.landscape = True
        else:
            self.landscape = False

    def getFramebuffer(self):
        return self.framebuffer

    def rotateFramebuffer( self, buf ):
        '''The SH1107 controller chip has no methods to rotate the screen.
        This is why we have to do this ourselves if we want to use the display
        in landscape mode (which for most of the applications is nicer...)
        This is pretty horrible spaghetti code here...but it is fast. If you
        have a more elegant solution, try it out! (Matrix multiplication may be?)
        ''' 

        bufr = self.bufr
        for ip in range(0,16):
            for ic in range(0,8):
                bufr[ip*64+ic*8+7] = ((buf[ic*128+120-ip*8]&0x80)   ) + ((buf[ic*128+121-ip*8]&0x80)>>1) + ((buf[ic*128+122-ip*8]&0x80)>>2) + ((buf[ic*128+123-ip*8]&0x80)>>3) + ((buf[ic*128+124-ip*8]&0x80)>>4) + ((buf[ic*128+125-ip*8]&0x80)>>5) + ((buf[ic*128+126-ip*8]&0x80)>>6) + ((buf[ic*128+127-ip*8]&0x80)>>7)
                bufr[ip*64+ic*8+6] = ((buf[ic*128+120-ip*8]&0x40)<<1) + ((buf[ic*128+121-ip*8]&0x40)   ) + ((buf[ic*128+122-ip*8]&0x40)>>1) + ((buf[ic*128+123-ip*8]&0x40)>>2) + ((buf[ic*128+124-ip*8]&0x40)>>3) + ((buf[ic*128+125-ip*8]&0x40)>>4) + ((buf[ic*128+126-ip*8]&0x40)>>5) + ((buf[ic*128+127-ip*8]&0x40)>>6)
                bufr[ip*64+ic*8+5] = ((buf[ic*128+120-ip*8]&0x20)<<2) + ((buf[ic*128+121-ip*8]&0x20)<<1) + ((buf[ic*128+122-ip*8]&0x20)   ) + ((buf[ic*128+123-ip*8]&0x20)>>1) + ((buf[ic*128+124-ip*8]&0x20)>>2) + ((buf[ic*128+125-ip*8]&0x20)>>3) + ((buf[ic*128+126-ip*8]&0x20)>>4) + ((buf[ic*128+127-ip*8]&0x20)>>5)
                bufr[ip*64+ic*8+4] = ((buf[ic*128+120-ip*8]&0x10)<<3) + ((buf[ic*128+121-ip*8]&0x10)<<2) + ((buf[ic*128+122-ip*8]&0x10)<<1) + ((buf[ic*128+123-ip*8]&0x10)   ) + ((buf[ic*128+124-ip*8]&0x10)>>1) + ((buf[ic*128+125-ip*8]&0x10)>>2) + ((buf[ic*128+126-ip*8]&0x10)>>3) + ((buf[ic*128+127-ip*8]&0x10)>>4)
                bufr[ip*64+ic*8+3] = ((buf[ic*128+120-ip*8]&0x08)<<4) + ((buf[ic*128+121-ip*8]&0x08)<<3) + ((buf[ic*128+122-ip*8]&0x08)<<2) + ((buf[ic*128+123-ip*8]&0x08)<<1) + ((buf[ic*128+124-ip*8]&0x08)   ) + ((buf[ic*128+125-ip*8]&0x08)>>1) + ((buf[ic*128+126-ip*8]&0x08)>>2) + ((buf[ic*128+127-ip*8]&0x08)>>3)
                bufr[ip*64+ic*8+2] = ((buf[ic*128+120-ip*8]&0x04)<<5) + ((buf[ic*128+121-ip*8]&0x04)<<4) + ((buf[ic*128+122-ip*8]&0x04)<<3) + ((buf[ic*128+123-ip*8]&0x04)<<2) + ((buf[ic*128+124-ip*8]&0x04)<<1) + ((buf[ic*128+125-ip*8]&0x04)   ) + ((buf[ic*128+126-ip*8]&0x04)>>1) + ((buf[ic*128+127-ip*8]&0x04)>>2)
                bufr[ip*64+ic*8+1] = ((buf[ic*128+120-ip*8]&0x02)<<6) + ((buf[ic*128+121-ip*8]&0x02)<<5) + ((buf[ic*128+122-ip*8]&0x02)<<4) + ((buf[ic*128+123-ip*8]&0x02)<<3) + ((buf[ic*128+124-ip*8]&0x02)<<2) + ((buf[ic*128+125-ip*8]&0x02)<<1) + ((buf[ic*128+126-ip*8]&0x02)   ) + ((buf[ic*128+127-ip*8]&0x02)>>1)
                bufr[ip*64+ic*8+0] = ((buf[ic*128+120-ip*8]&0x01)<<7) + ((buf[ic*128+121-ip*8]&0x01)<<6) + ((buf[ic*128+122-ip*8]&0x01)<<5) + ((buf[ic*128+123-ip*8]&0x01)<<4) + ((buf[ic*128+124-ip*8]&0x01)<<3) + ((buf[ic*128+125-ip*8]&0x01)<<2) + ((buf[ic*128+126-ip*8]&0x01)<<1) + ((buf[ic*128+127-ip*8]&0x01)   )

    def copyFramebuf(self, buf = None):
        '''Copy a framebuffer to the display so that the contents gets displayed.
        The user can provide an external buffer. If this it not provided the 
        internal framebuffer will be used.'''

        if not buf:
            buf = self.buf

        if self.landscape:
            self.rotateFramebuffer( buf )
            buf = self.bufr

            # We need to write the data page by page (there are 16 pages for 16x8=128 pixels vertically) 
            # For every page we need to set the page and the start column address. Since we always transfer
            # the entire display we set the column address to '0' (see data sheet for the commands used)
            for ip in range(0, 16):
            self._wr_cmd(0xB0 + ip) # page addr
            self._wr_cmd(0x00)      # col addr low
            self._wr_cmd(0x10)      # col addr high

            # We copy the page data into a temporary buffer but the first byte will be 0x40
            # This will indicate to the controller that all following bytes are Pixel data. 
            # (We should use a memoryview to avoid the copying of the framebuffer every time
            # this would be more elegant...)

            tmp = bytearray( 129 )
            tmp[0] = 0x40
            for i in range(0,64):
            tmp[i+1] = buf[ip*64+i]

            # Finally we write the data to the controller chip. Look at the documentation of 
            # the "writeto" function
            self.i2c.writeto( ??? )

    def invert( self, inv ):
        ''' This inverts the display (black on white instead of 
        white on black). It is a command which you find in the 
        controller data sheet. Fill it in here.'''
        if inv:
            ???
        else:
            ???