From d47519f82a772c2a08d43e4938bf99304c653916 Mon Sep 17 00:00:00 2001 From: troyhy Date: Mon, 2 Sep 2024 15:05:11 +0300 Subject: [PATCH] first working example of image from module created with image_converter.py --- drivers/st7789/st7789_16bit.py | 287 +++++++++++++++++++++++++++++++++ gui/widgets/color_bitmap.py | 100 ++++++++++++ utils/image_converter.py | 1 + 3 files changed, 388 insertions(+) create mode 100644 drivers/st7789/st7789_16bit.py create mode 100644 gui/widgets/color_bitmap.py diff --git a/drivers/st7789/st7789_16bit.py b/drivers/st7789/st7789_16bit.py new file mode 100644 index 0000000..f93fd40 --- /dev/null +++ b/drivers/st7789/st7789_16bit.py @@ -0,0 +1,287 @@ +# st7789.py Driver for ST7789 LCD displays for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021-2024 Peter Hinch, Ihor Nehrutsa + +# Tested displays: +# Adafruit 1.3" 240x240 Wide Angle TFT LCD Display with MicroSD - ST7789 +# https://www.adafruit.com/product/4313 +# TTGO T-Display +# http://www.lilygo.cn/prod_view.aspx?TypeId=50044&Id=1126 + +# Based on +# Adfruit https://github.com/adafruit/Adafruit_CircuitPython_ST7789/blob/master/adafruit_st7789.py +# Also see st7735r_4bit.py for other source acknowledgements + +# SPI bus: default mode. Driver performs no read cycles. +# Datasheet table 6 p44 scl write cycle 16ns == 62.5MHz + +from time import sleep_ms # , ticks_us, ticks_diff +import framebuf +import gc +import micropython +import asyncio +from drivers.boolpalette import BoolPalette + + +# User orientation constants +LANDSCAPE = 0 # Default +REFLECT = 1 +USD = 2 +PORTRAIT = 4 +# Display types +GENERIC = (0, 0, 0) +TDISPLAY = (52, 40, 1) +PI_PICO_LCD_2 = (0, 0, 1) # Waveshare Pico LCD 2 determined by Mike Wilson. +DFR0995 = (34, 0, 0) # DFR0995 Contributed by @EdgarKluge +WAVESHARE_13 = (0, 0, 16) # Waveshare 1.3" 240x240 LCD contributed by Aaron Mittelmeier +ADAFRUIT_1_9 = (35, 0, PORTRAIT) # 320x170 TFT https://www.adafruit.com/product/5394 + +# ST7789 commands +_ST7789_SWRESET = b"\x01" +_ST7789_SLPIN = b"\x10" +_ST7789_SLPOUT = b"\x11" +_ST7789_NORON = b"\x13" +_ST7789_INVOFF = b"\x20" +_ST7789_INVON = b"\x21" +_ST7789_DISPOFF = b"\x28" +_ST7789_DISPON = b"\x29" +_ST7789_CASET = b"\x2a" +_ST7789_RASET = b"\x2b" +_ST7789_RAMWR = b"\x2c" +_ST7789_VSCRDEF = b"\x33" +_ST7789_COLMOD = b"\x3a" +_ST7789_MADCTL = b"\x36" +_ST7789_VSCSAD = b"\x37" +_ST7789_RAMCTL = b"\xb0" + +@micropython.viper +def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int, gscale: bool): + # rgb565 - 16bit/pixel + n: int = 0 + x: int = 0 + while length: + c = source[x] + p = c >> 4 # current pixel + q = c & 0x0F # next pixel + if gscale: + dest[n] = (p >> 1 | p << 4 | p << 9 | ((p & 0x01) << 15)) ^ 0xFFFF + n += 1 + dest[n] = (q >> 1 | q << 4 | q << 9 | ((q & 0x01) << 15)) ^ 0xFFFF + else: + dest[n] = lut[p] # current pixel + n += 1 + dest[n] = lut[q] # next pixel + n += 1 + x += 1 + length -= 1 + + +class ST7789(framebuf.FrameBuffer): + + #lut = bytearray(0xFF for _ in range(32)) # set all colors to BLACK + + # Convert r, g, b in range 0-255 to a 16 bit colour value rgb565. + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + # For some reason color must be inverted on this controller. + @staticmethod + def rgb(r, g, b): + return ((b & 0xF8) << 5 | (g & 0x1C) << 11 | (g & 0xE0) >> 5 | (r & 0xF8)) ^ 0xFFFF + +# @staticmethod +# def rgb(r, g, b): +# return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5) + + # rst and cs are active low, SPI is mode 0 + def __init__( + self, + spi, + cs, + dc, + rst, + height=240, + width=240, + disp_mode=LANDSCAPE, + init_spi=False, + display=GENERIC, + ): + if not 0 <= disp_mode <= 7: + raise ValueError("Invalid display mode:", disp_mode) + self._spi = spi # Clock cycle time for write 16ns 62.5MHz max (read is 150ns) + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._offset = display[:2] # display arg is (x, y, orientation) + orientation = display[2] # where x, y is the RAM offset + self._spi_init = init_spi # Possible user callback + self._lock = asyncio.Lock() + self._gscale = True # Interpret buffer as index into color LUT + self.mode = framebuf.RGB565 # Use 4bit greyscale. + self.palette = BoolPalette(self.mode) + gc.collect() + buf = bytearray(height * width * 2) # Reserve 16bit per pixel + self.mvb = memoryview(buf) + super().__init__(buf, width, height, self.mode) + self._linebuf = bytearray(self.width * 2) # 16 bit color out + self._init(disp_mode, orientation) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 163ms. Adafruit have various sleep delays + # where I can find no requirement in the datasheet. I removed them with + # other redundant code. + def _init(self, user_mode, orientation): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(_ST7789_SWRESET) # SW reset datasheet specifies 120ms before SLPOUT + sleep_ms(150) + cmd(_ST7789_SLPOUT) # SLPOUT: exit sleep mode + sleep_ms(10) # Adafruit delay 500ms (datsheet 5ms) + wcd(_ST7789_COLMOD, b"\x55") # _COLMOD 16 bit/pixel, 65Kbit color space + cmd(_ST7789_INVOFF) # INVOFF Adafruit turn inversion on. This driver fixes .rgb + cmd(_ST7789_NORON) # NORON Normal display mode + + # Table maps user request onto hardware values. index values: + # 0 Normal + # 1 Reflect + # 2 USD + # 3 USD reflect + # Followed by same for LANDSCAPE + if not orientation: + user_mode ^= PORTRAIT + # Hardware mappings + # d7..d5 of MADCTL determine rotation/orientation datasheet P124, P231 + # d5 = MV row/col exchange + # d6 = MX col addr order + # d7 = MY page addr order + # LANDSCAPE = 0 + # PORTRAIT = 0x20 + # REFLECT = 0x40 + # USD = 0x80 + mode = (0x60, 0xE0, 0xA0, 0x20, 0, 0x40, 0xC0, 0x80)[user_mode] + # Set display window depending on mode, .height and .width. + self.set_window(mode) + wcd(_ST7789_MADCTL, int.to_bytes(mode, 1, "little")) + cmd(_ST7789_DISPON) # DISPON. Adafruit then delay 500ms. + + # Define the mapping between RAM and the display. + # Datasheet section 8.12 p124. + def set_window(self, mode): + portrait, reflect, usd = 0x20, 0x40, 0x80 + rht = 320 + rwd = 240 # RAM ht and width + wht = self.height # Window (framebuf) dimensions. + wwd = self.width # In portrait mode wht > wwd + if mode & portrait: + xoff = self._offset[1] # x and y transposed + yoff = self._offset[0] + xs = xoff + xe = wwd + xoff - 1 + ys = yoff # y start + ye = wht + yoff - 1 # y end + if mode & reflect: + ys = rwd - wht - yoff + ye = rwd - yoff - 1 + if mode & usd: + xs = rht - wwd - xoff + xe = rht - xoff - 1 + else: # LANDSCAPE + xoff = self._offset[0] + yoff = self._offset[1] + xs = xoff + xe = wwd + xoff - 1 + ys = yoff # y start + ye = wht + yoff - 1 # y end + if mode & usd: + ys = rht - wht - yoff + ye = rht - yoff - 1 + if mode & reflect: + xs = rwd - wwd - xoff + xe = rwd - xoff - 1 + + # Col address set. + self._wcd(_ST7789_CASET, int.to_bytes((xs << 16) + xe, 4, "big")) + # Row address set + self._wcd(_ST7789_RASET, int.to_bytes((ys << 16) + ye, 4, "big")) + + def greyscale(self, gs=None): + if gs is not None: + self._gscale = gs + return self._gscale + + # @micropython.native # Made virtually no difference to timing. + def show(self): + # ts = ticks_us() + + bw = self.width *2 + end = self.height * self.width * 2 + buf = self.mvb + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + + self._dc(0) + self._cs(0) + self._spi.write(_ST7789_RAMWR) # RAMWR + self._dc(1) + for start in range(0, end , bw): + self._spi.write(buf[start : start + bw]) + self._cs(1) + + # Asynchronous refresh with support for reducing blocking time. + async def ddo_refresh(self, split=5): + async with self._lock: + lines, mod = divmod(self.height, split) # Lines per segment + if mod: + raise ValueError("Invalid do_refresh arg.") + #clut = ST7789.lut + wd = -(-self.width // 2) + lb = memoryview(self._linebuf) + cm = self._gscale # color False, greyscale True + buf = self.mvb + line = 0 + for n in range(split): + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._dc(0) + self._cs(0) + self._spi.write(b"\x3c" if n else b"\x2c") # RAMWR/Write memory continue + self._dc(1) + for start in range(wd * line, wd * (line + lines), wd): + #_lcopy(lb, buf[start:], 0xffff, wd, cm) # Copy and map colors + self._spi.write(buf[start : start + wd]) + line += lines + self._cs(1) + await asyncio.sleep(0) diff --git a/gui/widgets/color_bitmap.py b/gui/widgets/color_bitmap.py new file mode 100644 index 0000000..0d242a5 --- /dev/null +++ b/gui/widgets/color_bitmap.py @@ -0,0 +1,100 @@ +# bitmap.py Provides the BMG (bitmapped graphics) class +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2022 Peter Hinch + +# Graphics are created by [utils/image_converter.py](utils/image_converter.py). Images +# have variable PPB encoding and a palette which is decoded by bitmap widget +# Processed image is a python module that has the bitstream and the palette of the image. + +from gui.core.ugui import Widget +from gui.core.colors import * +from gui.core.ugui import ssd +import struct + +def little_to_big_endian(value): + # A Helper function to convert 16 bit int from little to big endian + # Pack the value as little-endian + packed = struct.pack(f'H', packed)[0] + +class ColorBitMap(Widget): + + def __init__( + self, + writer, + row, + col, + height, + width, + *, + fgcolor=None, + bgcolor=None, + bdcolor=RED, + ): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + + # Transform palette to match display color format + # this will be run for with all image palette members + palette_transformer_cb = lambda self, x: ~little_to_big_endian(x) & 0xffff + + def show(self): + if not super().show(True): # Draw or erase border + return + if self._value is None: + return + self.bitmap(self._value) + + def bitmap(self, bitmap, index=0): + """ + Draw a bitmap on display at the specified column and row + Image has to be a module that is generated with image_converter.py + + Args: + bitmap (bitmap_module): The module containing the bitmap to draw + index (int): Optional index of bitmap to draw from multiple bitmap + module + palette_transformer_cb (callable): Trans + """ + width = bitmap.WIDTH + height = bitmap.HEIGHT + bitmap_size = height * width + buffer_len = bitmap_size * 2 + bpp = bitmap.BPP + bs_bit = bpp * bitmap_size * index # if index > 0 else 0 + + if self.palette_transformer_cb is not None: + palette = list(map(self.palette_transformer_cb, bitmap.PALETTE)) + else: + palette = bitmap.PALETTE + + pixel = 0 + for i in range(0, buffer_len, 2): + color_index = 0 + for _ in range(bpp): + color_index = (color_index << 1) | ( + (bitmap.BITMAP[bs_bit >> 3] >> (7 - (bs_bit & 7))) & 1 + ) + bs_bit += 1 + color = palette[color_index] + + col = pixel % width + row = (pixel // width) % height + ssd.pixel(self.col + col, self.row + row, color) + pixel += 1 + + def _validate(self, fn): + if not str(type(fn)) == "": + raise ValueError("Value must be a module") + wd = fn.WIDTH + ht = fn.HEIGHT + if not (wd == self.width and ht == self.height): + raise ValueError( + f"Object dimensions {ht}x{wd} do not match widget {self.height}x{self.width}" + ) + + def value(self, fn=None): + if fn is not None: + self._validate(fn) # Throws on failure + return super().value(fn) + diff --git a/utils/image_converter.py b/utils/image_converter.py index ae9b067..9d50782 100755 --- a/utils/image_converter.py +++ b/utils/image_converter.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """ +This file is copied from: https://github.com/russhughes/st7789py_mpy/blob/7265925bd0c092e8105200d18b2dba9dfbc12c27/utils/image_converter.py Convert an image file to a python module for use with the bitmap method. Use redirection to save the output to a file. The image is converted to a bitmap using the number of bits per pixel you specify. The bitmap is saved as a python module that can be imported and used with the bitmap method.