diff --git a/pyproject.toml b/pyproject.toml index 41d0e00..d4959fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ ignore = [ "INP001", # File is part of an implicit namespace package. (Just a bit unnecessary) "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "RUF005", # collection-literal-concatenation + "FBT001", # A function taking a sinlge bool value is very common. # MH uses commented out code in the build process: "ERA", # Found commented-out code diff --git a/src/launcher/launcher.py b/src/launcher/launcher.py index 8b8b4c9..c7b1ac3 100644 --- a/src/launcher/launcher.py +++ b/src/launcher/launcher.py @@ -50,9 +50,9 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ _CONSTANTS: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -_MH_DISPLAY_WIDTH = const(320) -_MH_DISPLAY_HEIGHT = const(240) -_MH_DISPLAY_BACKLIGHT = const(42) +_MH_DISPLAY_WIDTH = const(240) +_MH_DISPLAY_HEIGHT = const(135) +_MH_DISPLAY_BACKLIGHT = const(38) _DISPLAY_WIDTH_HALF = const(_MH_DISPLAY_WIDTH//2) @@ -87,6 +87,7 @@ _SCROLL_ANIMATION_TIME = const(400) +_SCROLL_ANIMATION_QUICK = const(250) _ASCII_MAX = const(128) @@ -492,6 +493,7 @@ def __init__(self): self.x = _DISPLAY_WIDTH_HALF self.prev_x = 0 self.scroll_start_ms = time.ticks_ms() + self.anim = _SCROLL_ANIMATION_TIME self.force_update() # buffer for storing one custom icon @@ -531,7 +533,7 @@ def _animate_scroll(self) -> int: fac = time.ticks_diff( time.ticks_ms(), self.scroll_start_ms, - ) / _SCROLL_ANIMATION_TIME + ) / self.anim_time if fac >= 1: self.direction = 0 @@ -548,6 +550,9 @@ def start_scroll(self, direction=0): """Initialize the scrolling animation.""" if self.next_icon != self.drawn_icon: self.force_update() + self.anim_time = _SCROLL_ANIMATION_QUICK + else: + self.anim_time = _SCROLL_ANIMATION_TIME draw_scrollbar() draw_app_name() @@ -799,19 +804,19 @@ def main_loop(): new_keys = KB.get_new_keys() # mh_if CARDPUTER: - # # Cardputer should use extended movement keys in the launcher - # KB.ext_dir_keys(new_keys) + # Cardputer should use extended movement keys in the launcher + KB.ext_dir_keys(new_keys) # mh_end_if # mh_if touchscreen: - # add swipes to direcitonal input - touch_events = KB.get_touch_events() - for event in touch_events: - if hasattr(event, 'direction'): - if event.direction == 'RIGHT': - new_keys.append('LEFT') - elif event.direction == 'LEFT': - new_keys.append('RIGHT') + # # add swipes to direcitonal input + # touch_events = KB.get_touch_events() + # for event in touch_events: + # if hasattr(event, 'direction'): + # if event.direction == 'RIGHT': + # new_keys.append('LEFT') + # elif event.direction == 'LEFT': + # new_keys.append('RIGHT') # mh_end_if if new_keys: @@ -919,6 +924,9 @@ def main_loop(): if SYNCING_CLOCK: try_sync_clock() + # short sleep makes the animation look a little less flickery + time.sleep_ms(5) + # run the main loop! main_loop() diff --git a/src/lib/display/displaycore.py b/src/lib/display/displaycore.py new file mode 100644 index 0000000..d2ea974 --- /dev/null +++ b/src/lib/display/displaycore.py @@ -0,0 +1,626 @@ +"""The heart of MicroHydra graphics functionality.""" + + +import framebuf +from .palette import Palette +from lib.hydra.utils import get_instance + +# mh_if frozen: +# # frozen firmware must access the font as a module, +# # rather than a binary file. +# from font.utf8_8x8 import utf8 +# mh_end_if + + + +class DisplayCore: + """The core graphical functionality for the Display module.""" + + def __init__( + self, + width: int, + height: int, + *, + rotation: int = 0, + use_tiny_buf: bool = False, + reserved_bytearray: bytearray|None = None, + needs_swap: bool = True, + **kwargs): # noqa: ARG002 + """Create the DisplayCore. + + Args: + width (int): display width + height (int): display height + Kwargs: + rotation (int): + How to rotate the framebuffer (Default 0) + use_tiny_buf (bool): + Whether or not to use a smaller, 4bit framebuffer (rather than 16 bit). + If True, frame is stored in 4bits and converted line-by-line when `show` is called. + reserved_bytearray (bytearray|None): + A pre-allocated byte array to use for the framebuffer (rather than creating one on init). + needs_swap (bool): + Whether or not the RGB565 bytes must be swapped to show up correctly on the display. + **kwargs (Any): + Any other kwargs are captured and ignored. + This is an effort to allow any future/additional versions of this module to be more compatible. + """ + #init the fbuf + if reserved_bytearray is None: + # use_tiny_fbuf tells us to use a smaller framebuffer (4 bits per pixel rather than 16 bits) + if use_tiny_buf: + # round width up to 8 bits + size = (height * width) // 2 if (width % 8 == 0) else (height * (width + 1)) // 2 + reserved_bytearray = bytearray(size) + else: # full sized buffer + reserved_bytearray = bytearray(height*width*2) + + self.fbuf = framebuf.FrameBuffer( + reserved_bytearray, + # height and width are swapped when rotation is 1 or 3 + height if (rotation % 2 == 1) else width, + width if (rotation % 2 == 1) else height, + # use_tiny_fbuf uses GS4 format for less memory usage + framebuf.GS4_HMSB if use_tiny_buf else framebuf.RGB565, + ) + + self.palette = get_instance(Palette) + self.palette.use_tiny_buf = self.use_tiny_buf = use_tiny_buf + + # keep track of min/max y vals for writing to display + # this speeds up drawing significantly. + # only y value is currently used, because framebuffer is stored in horizontal lines, + # making y slices much simpler than x slices. + self._show_y_min = width if (rotation % 2 == 1) else height + self._show_y_max = 0 + + self.width = width + self.height = height + self.needs_swap = needs_swap + + + + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DisplayCore utils: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def reset_show_y(self) -> tuple[int, int]: + """Return and reset y boundaries.""" + # clamp min and max + y_min = max(self._show_y_min, 0) + y_max = min(self._show_y_max, self.height) + + self._show_y_min = self.height + self._show_y_max = 0 + + return y_min, y_max + + + @micropython.viper + def _set_show_y(self, y0: int, y1: int): + """Set/store minimum and maximum Y to show next time show() is called.""" + y_min = int(self._show_y_min) + y_max = int(self._show_y_max) + + if y_min > y0: + y_min = y0 + if y_max < y1: + y_max = y1 + + self._show_y_min = y_min + self._show_y_max = y_max + + + @micropython.viper + def _format_color(self, color: int) -> int: + """Swap color bytes if needed, do nothing otherwise.""" + if (not self.use_tiny_buf) and self.needs_swap: + color = ((color & 0xff) << 8) | (color >> 8) + return color + + + def blit_buffer( + self, + buffer: bytearray|framebuf.FrameBuffer, + x: int, + y: int, + width: int, + height: int, + *, + key: int = -1, + palette: framebuf.FrameBuffer|None = None): + """Copy buffer to display framebuf at the given location. + + Args: + buffer (bytearray): Data to copy to display + x (int): Top left corner x coordinate + Y (int): Top left corner y coordinate + width (int): Width + height (int): Height + key (int): color to be considered transparent + palette (framebuf): the color pallete to use for the buffer + """ + self._set_show_y(y, y + height) + if not isinstance(buffer, framebuf.FrameBuffer): + buffer = framebuf.FrameBuffer( + buffer, width, height, + framebuf.GS4_HMSB if self.use_tiny_buf else framebuf.RGB565, + ) + + self.fbuf.blit(buffer, x, y, key, palette) + + + + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FrameBuffer Primitives: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def fill(self, color: int): + """Fill the entire FrameBuffer with the specified color. + + Args: + color (int): 565 encoded color + """ + # whole display must show + self._set_show_y(0, self.height) + color = self._format_color(color) + self.fbuf.fill(color) + + + def pixel(self, x: int, y: int, color: int): + """Draw a pixel at the given location and color. + + Args: + x (int): x coordinate + Y (int): y coordinate + color (int): 565 encoded color + """ + self._set_show_y(y, y) + color = self._format_color(color) + self.fbuf.pixel(x,y,color) + + + def vline(self, x: int, y: int, length: int, color: int): + """Draw vertical line at the given location and color. + + Args: + x (int): x coordinate + Y (int): y coordinate + length (int): length of line + color (int): 565 encoded color + """ + self._set_show_y(y, y + length) + color = self._format_color(color) + self.fbuf.vline(x, y, length, color) + + + def hline(self, x: int, y: int, length: int, color: int): + """Draw horizontal line at the given location and color. + + Args: + x (int): x coordinate + Y (int): y coordinate + length (int): length of line + color (int): 565 encoded color + """ + self._set_show_y(y, y) + color = self._format_color(color) + self.fbuf.hline(x, y, length, color) + + + def line(self, x0: int, y0: int, x1: int, y1: int, color: int): + """ + Draw a single pixel wide line starting at x0, y0 and ending at x1, y1. + + Args: + x0 (int): Start point x coordinate + y0 (int): Start point y coordinate + x1 (int): End point x coordinate + y1 (int): End point y coordinate + color (int): 565 encoded color + """ + self._set_show_y( + min(y0,y1), + max(y0,y1), + ) + color = self._format_color(color) + self.fbuf.line(x0, y0, x1, y1, color) + + + def rect(self, x: int, y: int, w: int, h: int, color: int, *, fill: bool = False): + """Draw a rectangle at the given location, size and color. + + Args: + x (int): Top left corner x coordinate + y (int): Top left corner y coordinate + width (int): Width in pixels + height (int): Height in pixels + color (int): 565 encoded color + """ + self._set_show_y(y, y + h) + color = self._format_color(color) + self.fbuf.rect(x,y,w,h,color,fill) + + + def fill_rect(self, x:int, y:int, width:int, height:int, color:int): + """Draw a rectangle at the given location, size and filled with color. + + This is just a wrapper for the rect() method, + and is provided for some compatibility with the original st7789py driver. + """ + self.rect(x, y, width, height, color, fill=True) + + + def ellipse(self, x:int, y:int, xr:int, yr:int, color:int, *, fill:bool=False, m:int=0xf): + """Draw an ellipse at the given location, radius and color. + + Args: + x (int): Center x coordinate + y (int): Center y coordinate + xr (int): x axis radius + yr (int): y axis radius + color (int): 565 encoded color + fill (bool): fill in the ellipse. Default is False + """ + self._set_show_y(y - yr, y + yr + 1) + color = self._format_color(color) + self.fbuf.ellipse(x,y,xr,yr,color,fill,m) + + + def polygon(self, coords, x: int, y: int, color: int, *, fill: bool = False): + """Draw a polygon from an array of coordinates. + + Args: + coords (array('h')): An array of x/y coordinates defining the shape + x (int): column to start drawing at + y (int): row to start drawing at + color (int): Color of polygon + fill (bool=False) : fill the polygon (or draw an outline) + """ + # calculate approx height so min/max can be set + h = max(coords) + self._set_show_y(y, y + h) + color = self._format_color(color) + self.fbuf.poly(x, y, coords, color, fill) + + + def scroll(self, xstep: int, ystep: int): + """Shift the contents of the FrameBuffer by the given vector. + + This is a wrapper for the framebuffer.scroll method. + """ + self._set_show_y(0, self.height) + self.fbuf.scroll(xstep,ystep) + + + + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Text Drawing: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def text(self, text: str, x: int, y: int, color: int, font=None): + """Draw text to the framebuffer. + + Text is drawn with no background. + If 'font' is None, uses the built-in font. + + Args: + text (str): text to write + x (int): column to start drawing at + y (int): row to start drawing at + color (int): encoded color to use for text + font (optional): bitmap font module to use + """ + color = self._format_color(color) + + if font: + self._set_show_y(y, y + font.HEIGHT) + self._bitmap_text(font, text, x, y, color) + else: + self._set_show_y(y, y + 8) + self._utf8_text(text, x, y, color) + + + @micropython.viper + def _bitmap_text(self, font, text, x:int, y:int, color:int): + """Quickly draw a text with a bitmap font using viper. + + Designed to be envoked using the 'text' method. + """ + width = int(font.WIDTH) + height = int(font.HEIGHT) + self_width = int(self.width) + self_height = int(self.height) + + utf8_scale = height // 8 + + # early return for text off screen + if y >= self_height or (y + height) < 0: + return + + glyphs = ptr8(font.FONT) + + char_px_len = width * height + + first = int(font.FIRST) + last = int(font.LAST) + + use_tiny_fbuf = bool(self.use_tiny_buf) + fbuf16 = ptr16(self.fbuf) + fbuf8 = ptr8(self.fbuf) + + for char in text: + ch_idx = int(ord(char)) + + # only draw chars that exist in font + if first <= ch_idx < last: + bit_start = (ch_idx - first) * char_px_len + + px_idx = 0 + while px_idx < char_px_len: + byte_idx = (px_idx + bit_start) // 8 + shift_amount = 7 - ((px_idx + bit_start) % 8) + + target_x = x + px_idx % width + target_y = y + px_idx // width + + # dont draw pixels off the screen (ptrs don't check your work!) + if ((glyphs[byte_idx] >> shift_amount) & 0x1) == 1 \ + and 0 <= target_x < self_width \ + and 0 <= target_y < self_height: + target_px = (target_y * self_width) + target_x + + # I tried putting this if/else before px loop, + # surprisingly, there was not a noticable speed difference, + # and the code was harder to read. So, I put it back. + if use_tiny_fbuf: + # pack 4 bits into 8 bit ptr + target_idx = target_px // 2 + dest_shift = ((target_px + 1) % 2) * 4 + dest_mask = 0xf0 >> dest_shift + fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (color << dest_shift) + else: + # draw to 16 bits + target_idx = target_px + fbuf16[target_idx] = color + + px_idx += 1 + x += width + else: + # try drawing with utf8 instead + x += int(self._utf8_putc(ch_idx, x, y, color, utf8_scale)) + + # early return for text off screen + if x >= self_width: + return + + + @micropython.viper + def _utf8_putc(self, char:int, x:int, y:int, color:int, scale:int) -> int: + """Render a single UTF8 character on the screen.""" + width = 4 if char < 128 else 8 + height = 8 + + if not 0x0000 <= char <= 0xFFFF: + return width * scale + + # set up viper variables + use_tiny_fbuf = bool(self.use_tiny_buf) + fbuf16 = ptr16(self.fbuf) + fbuf8 = ptr8(self.fbuf) + self_width = int(self.width) + self_height = int(self.height) + + # calculate the offset in the binary data + offset = char * 8 + + # mh_if frozen: + # # Read the font data directly from the memoryview + # cur = ptr8(utf8) + # mh_else: + # seek to offset and read 8 bytes + self.utf8_font.seek(offset) + cur = ptr8(self.utf8_font.read(8)) + # mh_end_if + + # y axis is inverted - we start from bottom not top + y += (height - 1) * scale - 1 + + # iterate over every character pixel + px_idx = 0 + max_px_idx = width * height + while px_idx < max_px_idx: + # which byte to fetch from the ptr8, + # and how far to shift (to get 1 bit) + ptr_idx = px_idx // 8 + shft_idx = px_idx % 8 + # mh_if frozen: + # # if reading from memoryview, add offset now + # ptr_idx += offset + # mh_end_if + + # calculate x/y position from pixel index + target_x = x + ((px_idx % width) * scale) + target_y = y - ((px_idx // width) * scale) + + if (cur[ptr_idx] >> shft_idx) & 1 == 1: + # iterate over x/y scale + scale_idx = 0 + num_scale_pixels = scale * scale + while scale_idx < num_scale_pixels: + xsize = scale_idx % scale + ysize = scale_idx // scale + + target_px = ((target_y + ysize) * self_width) + target_x + xsize + if 0 <= (target_x + xsize) < self_width \ + and 0 <= (target_y + ysize) < self_height: + if use_tiny_fbuf: + # pack 4 bits into 8 bit ptr + target_idx = target_px // 2 + dest_shift = ((target_px + 1) % 2) * 4 + dest_mask = 0xf0 >> dest_shift + fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (color << dest_shift) + else: + # draw to 16 bits + target_idx = target_px + fbuf16[target_idx] = color + scale_idx += 1 + px_idx += 1 + + # return x offset for drawing next char + return width * scale + + + @micropython.viper + def _utf8_text(self, text, x:int, y:int, color:int): + """Draw text, including utf8 characters.""" + str_len = int(len(text)) + + idx = 0 + while idx < str_len: + char = text[idx] + ch_ord = int(ord(char)) + if ch_ord >= 128: + x += int(self._utf8_putc(ch_ord, x, y, color, 1)) + else: + self.fbuf.text(char, x, y, color) + x += 8 + idx += 1 + + + @staticmethod + def get_total_width(text: str, *, scale: int = 8) -> int: + """Get the total width of a line (with UTF8 chars). + + Args: + text (str): The text string to measure. + scale (int): Optional width of each (single-width) character. + """ + return DisplayCore._get_total_width(text, len(bytes(text, "utf-8")), scale) + + + @staticmethod + @micropython.viper + def _get_total_width(text_ptr: ptr8, text_len: int, scale: int) -> int: + """Fast viper component of get_total_width. + + Scans over raw string bytes to count number of single-byte or multi-byte characters. + Saves a lot of time by not fully decoding any code-points, + and by skipping function call overhead from `ord`. + """ + total_width = 0 + idx = 0 + while idx < text_len: + # count leading 1's to determine byte type + # 0 = Single Byte, 1 = continuation byte + # 2-4 = Start byte (stop testing after 2) + leading_bytes = 0 + byte_shift = 7 + while leading_bytes < 2: + if text_ptr[idx] >> byte_shift & 1: + leading_bytes += 1 + byte_shift -= 1 + else: + break + + # single byte chars have width 8, others have width 16 + # ignore continuation bytes. + if leading_bytes == 0: # single byte char + total_width += 1 + elif leading_bytes > 1: # multi byte char + total_width += 2 + + idx += 1 + + return total_width * scale + + + + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bitmap Drawing: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def bitmap( + self,bitmap, x: int, + y: int, + *, + index: int = 0, + key: int = -1, + palette: list[int]|None = None): + """Draw a bitmap on display at the specified column and row. + + Args: + bitmap (bitmap_module): The module containing the bitmap to draw + x (int): column to start drawing at + y (int): row to start drawing at + index (int): Optional index of bitmap to draw from multiple bitmap + module + key (int): colors that match the key will be transparent. + """ + if self.width <= x or self.height <= y: + return + + if palette is None: + palette = bitmap.PALETTE + + self._bitmap(bitmap, x, y, index, key, palette) + + + @micropython.viper + def _bitmap(self, bitmap, x:int, y:int, index:int, key:int, palette): + + width = int(bitmap.WIDTH) + height = int(bitmap.HEIGHT) + self_width = int(self.width) + self_height = int(self.height) + + palette_len = int(len(palette)) + bpp = int(bitmap.BPP) + bitmap_pixels = height * width + starting_bit = bpp * bitmap_pixels * index # if index > 0 else 0 + + use_tiny_buf = bool(self.use_tiny_buf) + + self._set_show_y(y, y + height) + + # format color palette into a pointer + palette_buf = bytearray(palette_len * 2) + palette_ptr = ptr16(palette_buf) + for i in range(palette_len): + palette_ptr[i] = int( + self._format_color(palette[i]) + ) + key = int(self._format_color(key)) + + bitmap_ptr = ptr8(bitmap.BITMAP) + fbuf8 = ptr8(self.fbuf) + fbuf16 = ptr16(self.fbuf) + + bitmask = 0xffff >> (16 - bpp) + + # iterate over pixels + px_idx = 0 + while px_idx < bitmap_pixels: + source_bit = (px_idx * bpp) + starting_bit + source_idx = source_bit // 8 + source_shift = 7 - (source_bit % 8) + + # bitmap value is an index in the color palette + source = (bitmap_ptr[source_idx] >> source_shift) & bitmask + clr = palette_ptr[source] + + target_x = x + px_idx % width + target_y = y + px_idx // width + + # dont draw pixels off the screen (ptrs don't check your work!) + if clr != key \ + and 0 <= target_x < self_width \ + and 0 <= target_y < self_height: + + # convert px coordinate to an index + target_px = (target_y * self_width) + target_x + + if use_tiny_buf: + # writing 4-bit pixels + target_idx = target_px // 2 + dest_shift = ((target_px + 1) % 2) * 4 + dest_mask = 0xf0 >> dest_shift + fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (clr << dest_shift) + else: + # writing 16-bit pixels + target_idx = target_px + fbuf16[target_idx] = clr + + px_idx += 1 diff --git a/src/lib/display/st7789.py b/src/lib/display/st7789.py index 327a9cd..31c99db 100644 --- a/src/lib/display/st7789.py +++ b/src/lib/display/st7789.py @@ -40,22 +40,6 @@ - 320x240, 240x240, 135x240 and 128x128 pixel displays - Display rotation - RGB and BGR color orders -- Hardware based scrolling -- Drawing text using 8 and 16 bit wide bitmap fonts with heights that are - multiples of 8. Included are 12 bitmap fonts derived from classic pc - BIOS text mode fonts. -- Drawing converted bitmaps -- Named color constants - - - BLACK - - BLUE - - RED - - GREEN - - CYAN - - MAGENTA - - YELLOW - - WHITE - """ import struct @@ -64,6 +48,7 @@ import framebuf from .palette import Palette +from .displaycore import DisplayCore # mh_if frozen: @@ -134,38 +119,31 @@ # fmt: off # Rotation tables -# (madctl, width, height, xstart, ystart, needs_swap)[rotation % 4] +# (madctl, width, height, xstart, ystart)[rotation % 4] _DISPLAY_240x320 = const(( - (0x00, 240, 320, 0, 0, False), - (0x60, 320, 240, 0, 0, False), - (0xc0, 240, 320, 0, 0, False), - (0xa0, 320, 240, 0, 0, False))) + (0x00, 240, 320, 0, 0), + (0x60, 320, 240, 0, 0), + (0xc0, 240, 320, 0, 0), + (0xa0, 320, 240, 0, 0))) _DISPLAY_240x240 = const(( - (0x00, 240, 240, 0, 0, False), - (0x60, 240, 240, 0, 0, False), - (0xc0, 240, 240, 0, 80, False), - (0xa0, 240, 240, 80, 0, False))) + (0x00, 240, 240, 0, 0), + (0x60, 240, 240, 0, 0), + (0xc0, 240, 240, 0, 80), + (0xa0, 240, 240, 80, 0))) _DISPLAY_135x240 = const(( - (0x00, 135, 240, 52, 40, False), - (0x60, 240, 135, 40, 53, False), - (0xc0, 135, 240, 53, 40, False), - (0xa0, 240, 135, 40, 52, False))) + (0x00, 135, 240, 52, 40), + (0x60, 240, 135, 40, 53), + (0xc0, 135, 240, 53, 40), + (0xa0, 240, 135, 40, 52))) _DISPLAY_128x128 = const(( - (0x00, 128, 128, 2, 1, False), - (0x60, 128, 128, 1, 2, False), - (0xc0, 128, 128, 2, 1, False), - (0xa0, 128, 128, 1, 2, False))) - -# index values into rotation table -_WIDTH = const(0) -_HEIGHT = const(1) -_XSTART = const(2) -_YSTART = const(3) -_NEEDS_SWAP = const(4) + (0x00, 128, 128, 2, 1), + (0x60, 128, 128, 1, 2), + (0xc0, 128, 128, 2, 1), + (0xa0, 128, 128, 1, 2))) # Supported displays (physical width, physical height, rotation table) _SUPPORTED_DISPLAYS = const(( @@ -201,115 +179,44 @@ -class ST7789: - """ST7789 driver class. - - Args: - spi (spi): spi object **Required** - width (int): display width **Required** - height (int): display height **Required** - reset (pin): reset pin - dc (pin): dc pin **Required** - cs (pin): cs pin - backlight(pin): backlight pin - reserved_bytearray (bytearray): pre-allocated bytearray to use for framebuffer - use_tiny_buf (bool): - - Whether to use: - - A compact framebuffer (uses ~width * height / 2 bytes memory) - "GS4_HMSB" mode - Requires additional processing to write to display - Allows limited colors - - - A normal framebuffer (uses ~width * height * 2 bytes memory) - "RGB565" mode - Can be written directly to display - Allows any color the display can show - - rotation (int): - - - 0-Portrait - - 1-Landscape - - 2-Inverted Portrait - - 3-Inverted Landscape - - color_order (int): - - - RGB: Red, Green Blue, default - - BGR: Blue, Green, Red - - custom_init (tuple): custom initialization commands - - - ((b'command', b'data', delay_ms), ...) +class ST7789(DisplayCore): + """ST7789 driver class.""" - custom_rotations (tuple): custom rotation definitions + def __init__( + self, + spi, + width, + height, + *, + reset=None, + dc=None, + cs=None, + backlight=None, + rotation=0, + color_order='BGR', + **kwargs): + """Initialize display. - - ((width, height, xstart, ystart, madctl, needs_swap), ...) - """ + Args: + spi (spi): spi object **Required** + width (int): display width **Required** + height (int): display height **Required** + reset (pin): reset pin + dc (pin): dc pin **Required** + cs (pin): cs pin - def __init__( - self, - spi, - width, - height, - *, - reset=None, - dc=None, - cs=None, - backlight=None, - rotation=0, - color_order='BGR', - custom_init=None, - custom_rotations=None, - reserved_bytearray = None, - use_tiny_buf = False, - **kwargs, # noqa: ARG002 - ): - """Initialize display.""" - self.rotations = custom_rotations or self._find_rotations(width, height) - if not self.rotations: - supported_displays = ", ".join( - [f"{display[0]}x{display[1]}" for display in _SUPPORTED_DISPLAYS] - ) - msg = f"Unsupported {width}x{height} display. Supported displays: {supported_displays}" - raise ValueError(msg) - - if dc is None: - msg = "dc pin is required." - raise ValueError(msg) - - #init the fbuf - if reserved_bytearray is None: - # use_tiny_fbuf tells us to use a smaller framebuffer (4 bits per pixel rather than 16 bits) - if use_tiny_buf: - # round width up to 8 bits - size = (height * width) // 2 if (width % 8 == 0) else (height * (width + 1)) // 2 - reserved_bytearray = bytearray(size) - - else: # full sized buffer - reserved_bytearray = bytearray(height*width*2) - - self.fbuf = framebuf.FrameBuffer( - reserved_bytearray, - # height and width are swapped when rotation is 1 or 3 - height if (rotation % 2 == 1) else width, - width if (rotation % 2 == 1) else height, - # use_tiny_fbuf uses GS4 format for less memory usage - framebuf.GS4_HMSB if use_tiny_buf else framebuf.RGB565, - ) + rotation (int): + - 0-Portrait + - 1-Landscape + - 2-Inverted Portrait + - 3-Inverted Landscape - self.palette = Palette() - self.palette.use_tiny_buf = self.use_tiny_buf = use_tiny_buf + color_order (literal['RGB'|'BGR']): + """ + self.rotations = self._find_rotations(width, height) - # keep track of min/max y vals for writing to display - # this speeds up drawing significantly. - # only y value is currently used, because framebuffer is stored in horizontal lines, - # making y slices much simpler than x slices. - self._show_y_min = width if (rotation % 2 == 1) else height - self._show_y_max = 0 + super().__init__(width, height, rotation=rotation, **kwargs) - self.width = width - self.height = height self.xstart = 0 self.ystart = 0 self.spi = spi @@ -319,13 +226,11 @@ def __init__( self.backlight = backlight self._rotation = rotation % 4 self.color_order = _RGB if color_order == "RGB" else _BGR - init_cmds = custom_init or _ST7789_INIT_CMDS self.hard_reset() # yes, twice, once is not always enough - self.init(init_cmds) - self.init(init_cmds) + self.init(_ST7789_INIT_CMDS) + self.init(_ST7789_INIT_CMDS) self.rotation(self._rotation) - self.needs_swap = True self.fill(0x0) self.show() @@ -339,52 +244,21 @@ def __init__( @staticmethod - def _find_rotations(width, height): + def _find_rotations(width: int, height: int) -> tuple: for display in _SUPPORTED_DISPLAYS: if display[0] == width and display[1] == height: return display[2] - return None + msg = f"{width}x{height} display. Not in `_SUPPORTED_DISPLAYS`" + raise ValueError(msg) - def init(self, commands): - """ - Initialize display. - """ + def init(self, commands: tuple): + """Initialize display.""" for command, data, delay in commands: self._write(command, data) sleep_ms(delay) - def _reset_show_min(self): - """Reset show boundaries""" - self._show_y_min = self.height - self._show_y_max = 0 - - - @micropython.viper - def _set_show_min(self, y0:int, y1:int): - """Set/store minimum and maximum Y to show next time show() is called.""" - y_min = int(self._show_y_min) - y_max = int(self._show_y_max) - - - if y_min > y0: - y_min = y0 - if y_max < y1: - y_max = y1 - - self._show_y_min = y_min - self._show_y_max = y_max - - - @micropython.viper - def _format_color(self, color:int) -> int: - """Swap color bytes if needed, do nothing otherwise.""" - if (not self.use_tiny_buf) and self.needs_swap: - color = ((color & 0xff) << 8) | (color >> 8) - return color - - def _write(self, command=None, data=None): """SPI write to the device: commands and data.""" if self.cs: @@ -397,24 +271,25 @@ def _write(self, command=None, data=None): self.spi.write(data) if self.cs: self.cs.on() - - + + @micropython.viper - def _write_tiny_buf(self): - """Convert tiny_buf data to RGB565 and write to SPI""" + def _write_tiny_buf(self, y_min: int, y_max: int): + """Convert tiny_buf data to RGB565 and write to SPI. + + This Viper method iterates over each line from y_min to y_max, + converts the 4bit data to 16bit RGB565 format, + and sends the data over SPI. + """ if self.cs: self.cs.off() self.dc.on() - landscape_rotation = int(self._rotation) % 2 == 1 - - height = int(self.height) width = int(self.width) - - start_y = int(self._show_y_min) - end_y = int(self._show_y_max) - - # swap colors if needed + start_y = int(y_min) + end_y = int(y_max) + + # swap colors in palette if needed if self.needs_swap: palette_buf = bytearray(32) target_palette_ptr = ptr16(palette_buf) @@ -422,59 +297,48 @@ def _write_tiny_buf(self): for i in range(16): target_palette_ptr[i] = ((source_palette_ptr[i] & 255) << 8) | (source_palette_ptr[i] >> 8) else: - palette_buf = self.palette.buf - - #for y in range(start_y, end_y): - while start_y < end_y: - self.spi.write( - self._convert_tiny_line(palette_buf, start_y, width) - ) - start_y += 1 - - if self.cs: - self.cs.on() + target_palette_ptr = ptr16(self.palette.buf) - @micropython.viper - def _convert_tiny_line(self, palette_buf, y:int, width:int): - """ - For "_write_tiny_buf" - this method outputs a single converted line of the requested size. - """ + # prepare variables for line conversion loop: source_ptr = ptr8(self.fbuf) - palette = ptr16(palette_buf) + source_width = width // 2 if (width % 8 == 0) else ((width + 1) // 2) output_buf = bytearray(width * 2) output = ptr16(output_buf) - - - source_width = width // 2 if (width % 8 == 0) else ((width + 1) // 2) - source_start_idx = source_width * y - output_idx = 0 - - while output_idx < width: - source_idx = source_start_idx + (output_idx // 2) - sample = source_ptr[source_idx] >> 4 if (output_idx % 2 == 0) else source_ptr[source_idx] & 0xf - output[output_idx] = palette[sample] - - output_idx += 1 - - return output_buf + # Iterate (vertically) over each horizontal line in given range: + while start_y < end_y: + source_start_idx = source_width * start_y + output_idx = 0 + # Iterate over horizontal pixels: + while output_idx < width: + # Calculate source pixel location, and sample it. + source_idx = source_start_idx + (output_idx // 2) + sample = source_ptr[source_idx] >> 4 if (output_idx % 2 == 0) else source_ptr[source_idx] & 0xf + + output[output_idx] = target_palette_ptr[sample] + output_idx += 1 + + # Write buffer to SPI + self.spi.write(output_buf) + + start_y += 1 + + if self.cs: + self.cs.on() - def _write_normal_buf(self): + def _write_normal_buf(self, y_min: int, y_max: int): """Write normal framebuf data, respecting show_y_min/max values.""" - source_start_idx = self._show_y_min * self.width * 2 - source_end_idx = self._show_y_max * self.width * 2 - + source_start_idx = y_min * self.width * 2 + source_end_idx = y_max * self.width * 2 + if source_start_idx < source_end_idx: self._write(None, memoryview(self.fbuf)[source_start_idx:source_end_idx]) def hard_reset(self): - """ - Hard reset display. - """ + """Hard reset display.""" if self.cs: self.cs.off() if self.reset: @@ -491,14 +355,12 @@ def hard_reset(self): def soft_reset(self): - """ - Soft reset display. - """ + """Soft reset display.""" self._write(_ST7789_SWRESET) sleep_ms(150) - def sleep_mode(self, value): + def sleep_mode(self, value: bool): """ Enable or disable display sleep mode. @@ -507,19 +369,19 @@ def sleep_mode(self, value): mode """ # mh_if TDECK: - # TDeck shares SPI with SDCard - self.spi.init() + # # TDeck shares SPI with SDCard + # self.spi.init() # mh_end_if if value: self._write(_ST7789_SLPIN) else: self._write(_ST7789_SLPOUT) # mh_if TDECK: - self.spi.deinit() + # self.spi.deinit() # mh_end_if - def inversion_mode(self, value): + def inversion_mode(self, value: bool): """ Enable or disable display inversion mode. @@ -528,15 +390,15 @@ def inversion_mode(self, value): inversion mode """ # mh_if TDECK: - # TDeck shares SPI with SDCard - self.spi.init() + # # TDeck shares SPI with SDCard + # self.spi.init() # mh_end_if if value: self._write(_ST7789_INVON) else: self._write(_ST7789_INVOFF) # mh_if TDECK: - self.spi.deinit() + # self.spi.deinit() # mh_end_if @@ -561,7 +423,6 @@ def rotation(self, rotation): self.height, self.xstart, self.ystart, - self.needs_swap, ) = self.rotations[rotation] if self.color_order == _BGR: @@ -594,511 +455,33 @@ def _set_window(self, x0, y0, x1, y1): self._write(_ST7789_RAMWR) - def vline(self, x, y, length, color): - """ - Draw vertical line at the given location and color. - - Args: - x (int): x coordinate - Y (int): y coordinate - length (int): length of line - color (int): 565 encoded color - """ - self._set_show_min(y, y + length) - color = self._format_color(color) - self.fbuf.vline(x, y, length, color) - - - def hline(self, x, y, length, color): - """ - Draw horizontal line at the given location and color. - - Args: - x (int): x coordinate - Y (int): y coordinate - length (int): length of line - color (int): 565 encoded color - """ - self._set_show_min(y, y) - color = self._format_color(color) - self.fbuf.hline(x, y, length, color) - - - def pixel(self, x, y, color): - """ - Draw a pixel at the given location and color. - - Args: - x (int): x coordinate - Y (int): y coordinate - color (int): 565 encoded color - """ - self._set_show_min(y, y) - color = self._format_color(color) - self.fbuf.pixel(x,y,color) - - def show(self): - """ - Write the current framebuf to the display - """ - if self._show_y_min > self._show_y_max: - # nothing to show - return - + """Write the current framebuf to the display.""" # mh_if TDECK: - # TDeck shares SPI with SDCard - self.spi.init() + # # TDeck shares SPI with SDCard + # self.spi.init() # mh_end_if - # clamp min and max - if self._show_y_min < 0: - self._show_y_min = 0 - if self._show_y_max > self.height: - self._show_y_max = self.height + # Reset and clamp min/max vals + y_min, y_max = self.reset_show_y() + + if y_min >= y_max: + # nothing to show + return self._set_window( 0, - self._show_y_min, + y_min, self.width - 1, - self._show_y_max - 1, + y_max - 1, ) - + if self.use_tiny_buf: - self._write_tiny_buf() + self._write_tiny_buf(y_min, y_max) else: - self._write_normal_buf() - - self._reset_show_min() + self._write_normal_buf(y_min, y_max) # mh_if TDECK: - # TDeck shares SPI with SDCard - self.spi.deinit() + # # TDeck shares SPI with SDCard + # self.spi.deinit() # mh_end_if - - def blit_buffer(self, buffer, x, y, width, height, key=-1, palette=None): - """ - Copy buffer to display framebuf at the given location. - - Args: - buffer (bytes): Data to copy to display - x (int): Top left corner x coordinate - Y (int): Top left corner y coordinate - width (int): Width - height (int): Height - key (int): color to be considered transparent - palette (framebuf): the color pallete to use for the buffer - """ - self._set_show_min(y, y + height) - if not isinstance(buffer, framebuf.FrameBuffer): - buffer = framebuf.FrameBuffer( - buffer, width, height, - framebuf.GS4_HMSB if self.use_tiny_buf else framebuf.RGB565, - ) - - self.fbuf.blit(buffer, x, y, key, palette) - - - def rect(self, x, y, w, h, color, fill=False): - """ - Draw a rectangle at the given location, size and color. - - Args: - x (int): Top left corner x coordinate - y (int): Top left corner y coordinate - width (int): Width in pixels - height (int): Height in pixels - color (int): 565 encoded color - """ - self._set_show_min(y, y + h) - color = self._format_color(color) - self.fbuf.rect(x,y,w,h,color,fill) - - - def ellipse(self, x, y, xr, yr, color, fill=False, m=0xf): - """ - Draw an ellipse at the given location, radius and color. - - Args: - x (int): Center x coordinate - y (int): Center y coordinate - xr (int): x axis radius - yr (int): y axis radius - color (int): 565 encoded color - fill (bool): fill in the ellipse. Default is False - """ - self._set_show_min(y - yr, y + yr + 1) - color = self._format_color(color) - self.fbuf.ellipse(x,y,xr,yr,color,fill,m) - - - def fill_rect(self, x, y, width, height, color): - """ - Draw a rectangle at the given location, size and filled with color. - - This is just a wrapper for the rect() method, - and is provided for compatibility with the original st7789py driver. - - Args: - x (int): Top left corner x coordinate - y (int): Top left corner y coordinate - width (int): Width in pixels - height (int): Height in pixels - color (int): 565 encoded color - """ - self._set_show_min(y, y + height) - self.rect(x, y, width, height, color, fill=True) - - - def fill(self, color): - """ - Fill the entire FrameBuffer with the specified color. - - Args: - color (int): 565 encoded color - """ - # whole display must show - self._set_show_min(0, self.height) - color = self._format_color(color) - self.fbuf.fill(color) - - - def line(self, x0, y0, x1, y1, color): - """ - Draw a single pixel wide line starting at x0, y0 and ending at x1, y1. - - Args: - x0 (int): Start point x coordinate - y0 (int): Start point y coordinate - x1 (int): End point x coordinate - y1 (int): End point y coordinate - color (int): 565 encoded color - """ - self._set_show_min( - min(y0,y1), - max(y0,y1) - ) - color = self._format_color(color) - self.fbuf.line(x0, y0, x1, y1, color) - - - def scroll(self,xstep,ystep): - """ - Shift the contents of the FrameBuffer by the given vector. - This may leave a footprint of the previous colors in the FrameBuffer. - - Unlike vscsad which uses the hardware for scrolling, - this method scrolls the framebuffer itself. - This is a wrapper for the framebuffer.scroll method: - """ - self._set_show_min(0, self.height) - self.fbuf.scroll(xstep,ystep) - - - @micropython.viper - def _bitmap_text(self, font, text, x:int, y:int, color:int): - """ - Internal viper method to draw text. - Designed to be envoked using the 'text' method. - - Args: - font (module): font module to use - text (str): text to write - x (int): column to start drawing at - y (int): row to start drawing at - color (int): encoded color to use for characters - """ - width = int(font.WIDTH) - height = int(font.HEIGHT) - self_width = int(self.width) - self_height = int(self.height) - - utf8_scale = height // 8 - - # early return for text off screen - if y >= self_height or (y + height) < 0: - return - - glyphs = ptr8(font.FONT) - - char_px_len = width * height - - first = int(font.FIRST) - last = int(font.LAST) - - - use_tiny_fbuf = bool(self.use_tiny_buf) - fbuf16 = ptr16(self.fbuf) - fbuf8 = ptr8(self.fbuf) - - for char in text: - ch_idx = int(ord(char)) - - # only draw chars that exist in font - if first <= ch_idx < last: - bit_start = (ch_idx - first) * char_px_len - - px_idx = 0 - while px_idx < char_px_len: - byte_idx = (px_idx + bit_start) // 8 - shift_amount = 7 - ((px_idx + bit_start) % 8) - - target_x = x + px_idx % width - target_y = y + px_idx // width - - # dont draw pixels off the screen (ptrs don't check your work!) - if ((glyphs[byte_idx] >> shift_amount) & 0x1) == 1 \ - and 0 <= target_x < self_width \ - and 0 <= target_y < self_height: - target_px = (target_y * self_width) + target_x - - # I tried putting this if/else before px loop, - # surprisingly, there was not a noticable speed difference, - # and the code was harder to read. So, I put it back. - if use_tiny_fbuf: - # pack 4 bits into 8 bit ptr - target_idx = target_px // 2 - dest_shift = ((target_px + 1) % 2) * 4 - dest_mask = 0xf0 >> dest_shift - fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (color << dest_shift) - else: - # draw to 16 bits - target_idx = target_px - fbuf16[target_idx] = color - - px_idx += 1 - x += width - else: - # try drawing with utf8 instead - x += int(self.utf8_putc(ch_idx, x, y, color, utf8_scale)) - - # early return for text off screen - if x >= self_width: - return - - - @micropython.viper - def utf8_putc(self, char:int, x:int, y:int, color:int, scale:int) -> int: - """Render a single character on the screen.""" - width = 4 if char < 128 else 8 - height = 8 - - if not 0x0000 <= char <= 0xFFFF: - return width * scale - - # set up viper variables - use_tiny_fbuf = bool(self.use_tiny_buf) - fbuf16 = ptr16(self.fbuf) - fbuf8 = ptr8(self.fbuf) - self_width = int(self.width) - self_height = int(self.height) - - # calculate the offset in the binary data - offset = char * 8 - - # mh_if frozen: - # # Read the font data directly from the memoryview - # cur = ptr8(utf8) - # mh_else: - # seek to offset and read 8 bytes - self.utf8_font.seek(offset) - cur = ptr8(self.utf8_font.read(8)) - # mh_end_if - - # y axis is inverted - we start from bottom not top - y += (height - 1) * scale - 1 - - # iterate over every character pixel - px_idx = 0 - max_px_idx = width * height - while px_idx < max_px_idx: - # which byte to fetch from the ptr8, - # and how far to shift (to get 1 bit) - ptr_idx = px_idx // 8 - shft_idx = px_idx % 8 - # mh_if frozen: - # # if reading from memoryview, add offset now - # ptr_idx += offset - # mh_end_if - - # calculate x/y position from pixel index - target_x = x + ((px_idx % width) * scale) - target_y = y - ((px_idx // width) * scale) - - if (cur[ptr_idx] >> shft_idx) & 1 == 1: - # iterate over x/y scale - scale_idx = 0 - num_scale_pixels = scale * scale - while scale_idx < num_scale_pixels: - xsize = scale_idx % scale - ysize = scale_idx // scale - - target_px = ((target_y + ysize) * self_width) + target_x + xsize - if 0 <= (target_x + xsize) < self_width \ - and 0 <= (target_y + ysize) < self_height: - if use_tiny_fbuf: - # pack 4 bits into 8 bit ptr - target_idx = target_px // 2 - dest_shift = ((target_px + 1) % 2) * 4 - dest_mask = 0xf0 >> dest_shift - fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (color << dest_shift) - else: - # draw to 16 bits - target_idx = target_px - fbuf16[target_idx] = color - scale_idx += 1 - px_idx += 1 - - # return x offset for drawing next char - return width * scale - - - @micropython.viper - def _utf8_text(self, text, x:int, y:int, color:int): - """Draw text, including utf8 characters""" - str_len = int(len(text)) - - idx = 0 - while idx < str_len: - char = text[idx] - ch_ord = int(ord(char)) - if ch_ord >= 128: - x += int(self.utf8_putc(ch_ord, x, y, color, 1)) - else: - self.fbuf.text(char, x, y, color) - x += 8 - idx += 1 - - - def text(self, text, x, y, color, font=None): - """ - Draw text to the framebuffer. - - Text is drawn with no background. - If 'font' is None, uses the builtin framebuffer font. - - Args: - text (str): text to write - x (int): column to start drawing at - y (int): row to start drawing at - color (int): encoded color to use for text - font (optional): bitmap font module to use - """ - color = self._format_color(color) - - - if font: - self._set_show_min(y, y + font.HEIGHT) - self._bitmap_text(font, text, x, y, color) - else: - self._set_show_min(y, y + 8) - self._utf8_text(text, x, y, color) - - - def bitmap(self, bitmap, x, y, index=0, key=-1, palette=None): - """ - Draw a bitmap on display at the specified column and row - - Args: - bitmap (bitmap_module): The module containing the bitmap to draw - x (int): column to start drawing at - y (int): row to start drawing at - index (int): Optional index of bitmap to draw from multiple bitmap - module - key (int): colors that match the key will be transparent. - """ - if self.width <= x or self.height <= y: - return - - if palette is None: - palette = bitmap.PALETTE - - self._bitmap(bitmap, x, y, index, key, palette) - - - @micropython.viper - def _bitmap(self, bitmap, x:int, y:int, index:int, key:int, palette): - - width = int(bitmap.WIDTH) - height = int(bitmap.HEIGHT) - self_width = int(self.width) - self_height = int(self.height) - - palette_len = int(len(palette)) - bpp = int(bitmap.BPP) - bitmap_pixels = height * width - starting_bit = bpp * bitmap_pixels * index # if index > 0 else 0 - - use_tiny_buf = bool(self.use_tiny_buf) - - self._set_show_min(y, y + height) - - - # format color palette into a pointer - palette_buf = bytearray(palette_len * 2) - palette_ptr = ptr16(palette_buf) - for i in range(palette_len): - palette_ptr[i] = int( - self._format_color(palette[i]) - ) - key = int(self._format_color(key)) - - bitmap_ptr = ptr8(bitmap._bitmap) - fbuf8 = ptr8(self.fbuf) - fbuf16 = ptr16(self.fbuf) - - bitmask = 0xffff >> (16 - bpp) - - # iterate over pixels - px_idx = 0 - while px_idx < bitmap_pixels: - source_bit = (px_idx * bpp) + starting_bit - source_idx = source_bit // 8 - source_shift = 7 - (source_bit % 8) - - # bitmap value is an index in the color palette - source = (bitmap_ptr[source_idx] >> source_shift) & bitmask - clr = palette_ptr[source] - - target_x = x + px_idx % width - target_y = y + px_idx // width - - # dont draw pixels off the screen (ptrs don't check your work!) - if clr != key \ - and 0 <= target_x < self_width \ - and 0 <= target_y < self_height: - - # convert px coordinate to an index - target_px = (target_y * self_width) + target_x - - if use_tiny_buf: - # writing 4-bit pixels - target_idx = target_px // 2 - dest_shift = ((target_px + 1) % 2) * 4 - dest_mask = 0xf0 >> dest_shift - fbuf8[target_idx] = (fbuf8[target_idx] & dest_mask) | (clr << dest_shift) - else: - # TODO: TEST THIS! (has only been tested for tiny fbuf) - # writing 16-bit pixels - target_idx = target_px - fbuf16[target_idx] = clr - - px_idx += 1 - - - def polygon(self, coords, x, y, color, fill=False): - """ - Draw a polygon from an array of coordinates - - Args: - coords (array('h')): An array of x/y coordinates defining the shape - x (int): column to start drawing at - y (int): row to start drawing at - color (int): Color of polygon - fill (bool=False) : fill the polygon (or draw an outline) - """ - # calculate approx height so min/max can be set - h = max(coords) - self._set_show_min(y, y + h) - color = self._format_color(color) - self.fbuf.poly(x, y, coords, color, fill)