# Micropython driver for SH1107-based OLED display (64 x 128)
#
# Driver with the same API as micropython built-in SSD1306 driver:
# - I2C only
# - Tested with https://www.aliexpress.com/item/4000464960639.html; both in 128 x 64 and 64 x 128 pixels mode
# - Not tested in 128 x 128 pixel mode
# - Implements simple optimization to reduce the amount of data sent during show()
#
# https://github.com/nemart69/sh1107-micropython
# https://www.displayfuture.com/Display/datasheet/controller/SH1107.pdf

from micropython import const
from framebuf import FrameBuffer, MONO_VLSB, MONO_HMSB

SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_DCDC_MODE = const(0xAD)
SET_MEM_MODE = const(0x20)
SET_PAGE_ADDR = const(0xB0)
SET_COL_LO_ADDR = const(0x00)
SET_COL_HI_ADDR = const(0x10)
SET_DISP_START_LINE = const(0xDC)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)

TEST_CHUNK = const(8)

class SH1107(FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.external_vcc = external_vcc
        if width == 128 and height == 64:
            self.page_mode = False
        elif (width == 64 and height == 128) or (width == 128 and height == 128):
            self.page_mode = True
        else:
            raise ValueError
        self.width = width
        self.height = height
        self.pages = self.height // 8
        self.line_bytes = self.width // 8
        size = self.width * self.height // 8
        self.curr_buffer = bytearray(b'\x00' * size) # self.fill(0)
        self.prev_buffer = bytearray(b'\xff' * size) # force full refresh
        super().__init__(self.curr_buffer, self.width, self.height, MONO_VLSB if self.page_mode else MONO_HMSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP |           0x00, # off
            # address setting
            SET_MEM_MODE |       (0x00 if self.page_mode else 0x01), # 0x00 = page, 0x01 = vertical
            # resolution and layout
            SET_DISP_START_LINE, 0x00,
            SET_SEG_REMAP |      0x00, # 0x01 rotate 180 deg
            SET_COM_OUT_DIR |    (0x00 if self.page_mode else 0x08), # 0x08 rotate 180 deg
            SET_MUX_RATIO,       0x7f, # always this?
            SET_DISP_OFFSET,     0x60 if self.width != self.height else 0x00, # offseted for 64 x 128 (Aliexpress 0.96")
            # timing and driving scheme
            SET_DISP_CLK_DIV,    0x50,
            SET_PRECHARGE,       0x22 if self.external_vcc else 0xf1,
            SET_VCOM_DESEL,      0x35, # 0.77 * Vcc
            SET_DCDC_MODE,       0x81, # on, 0.6 * switch freq
            # display
            SET_CONTRAST,        0x10, # very low to avoid uneven background
            SET_ENTIRE_ON |      0x00, # output follows RAM contents, not entire on
            SET_NORM_INV |       0x00  # 0x00 = not inverted, 0x01 = inverted
        ):
            self.write_cmd(cmd)
        # buffers are initialized as if self.fill(0) was called
        self.show()
        self.poweron()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        if self.page_mode:
            self.show_page_mode()
        else:
            self.show_vert_mode()
        self.prev_buffer[:] = self.curr_buffer

    def show_page_mode(self):
        for page in range(self.pages):
            noffs = page * self.width
            for col1, col2 in self.test_modified(noffs, self.width):
                c = col1 - noffs
                self.write_cmd(SET_PAGE_ADDR | page)
                self.write_cmd(SET_COL_LO_ADDR | (c & 0x0f))
                self.write_cmd(SET_COL_HI_ADDR | ((c & 0x70) >> 4))
                self.write_data(self.curr_buffer[col1 : col2])
                # print('Write offsets {} : {}, col: {}'.format(col1, col2, c))

    def show_vert_mode(self):
        for col in range(self.height):
            noffs = col * self.line_bytes
            for page1, page2 in self.test_modified(noffs, self.line_bytes):
                self.write_cmd(SET_PAGE_ADDR | (page1 - noffs))
                self.write_cmd(SET_COL_LO_ADDR | (col & 0x0f))
                self.write_cmd(SET_COL_HI_ADDR | ((col & 0x70) >> 4))
                self.write_data(self.curr_buffer[page1 : page2])
                # print('Write offsets {} : {}, page: {}'.format(page1, page2, page1 - noffs))

    def test_modified(self, offs, width):
        ptr = offs
        width += offs
        while ptr < width:
            # skip unmodified chunks
            while ptr < width and self.curr_buffer[ptr : ptr + TEST_CHUNK] == self.prev_buffer[ptr : ptr + TEST_CHUNK]:
                ptr += TEST_CHUNK

            if ptr < width:
                first = ptr
                ptr += TEST_CHUNK
                # find modified chunks
                while ptr < width and self.curr_buffer[ptr : ptr + TEST_CHUNK] != self.prev_buffer[ptr : ptr + TEST_CHUNK]:
                    ptr += TEST_CHUNK

                yield first, ptr
                ptr += TEST_CHUNK


class SH1107_I2C(SH1107):
    def __init__(self, width, height, i2c, addr = 0x3C, external_vcc = False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b'\x40', None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)
