diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..f50ed9b --- /dev/null +++ b/boot.py @@ -0,0 +1,5 @@ +# This file is executed on every boot (including wake-boot from deepsleep) +#import esp +#esp.osdebug(None) +#import webrepl +#webrepl.start() diff --git a/display_program.py b/display_program.py new file mode 100644 index 0000000..eecaa52 --- /dev/null +++ b/display_program.py @@ -0,0 +1,95 @@ +import gc +import os +import time +from time import sleep, time_ns + +from machine import SPI, Pin + +from ili9341 import Display, color565 + +colors = { + "RED": (255, 0, 0), + "GREEN": (0, 255, 0), + "BLUE": (0, 0, 255), + "YELLOW": (255, 255, 0), + "AQUA": (0, 255, 255), + "MAROON": (128, 0, 0), + "DARK_GREEN": (0, 128, 0), + "NAVY": (0, 0, 128), +} + + +def display(): + bg = Pin(33, Pin.OUT) + spi = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13)) + display = Display(spi, dc=Pin(27), cs=Pin(25), rst=Pin(26)) + + bg.on() + + while True: + display.draw_text8x8( + 10, + 10, + "ILI9341 Display Test", + color565(255, 255, 255), + color565(0, 0, 0), + 270, + ) + display.draw_text8x8( + 10, + 280, + "PPAP", + color565(255, 255, 255), + color565(0, 0, 0), + 270, + ) + sleep(1) + # Calculate valid hlines parameters for display clear method + valid_hlines = [] + for i in range(1, display.height): + if display.height % i == 0: + valid_hlines.append(i) + # Ensure only 13 entries, truncate or repeat the last one + valid_hlines = valid_hlines[:8] + if len(valid_hlines) < 8: + valid_hlines += [valid_hlines[-1]] * (8 - len(valid_hlines)) + # Ensure only 13 entries, truncate or repeat the last one + valid_hlines = valid_hlines[:8] + if len(valid_hlines) < 8: + valid_hlines += [valid_hlines[-1]] * (8 - len(valid_hlines)) + + print("Clearing to black...") + start = time_ns() + display.clear() + end = time_ns() + print(f"Display cleared in {end - start} ms.") + sleep(2) + + print("Clearing to white...") + start = time_ns() + display.clear(color565(255, 255, 255)) + end = time_ns() + print(f"Display cleared in {end - start} ms.") + sleep(2) + + for hlines, (color, rgb) in zip(valid_hlines, colors.items()): + display.draw_text8x8( + 10, + 10, + f"Next color: {color}", + color565(255, 255, 255), + color565(0, 0, 0), + 270, + ) + sleep(1) + print(f"Clearing display to {color}, hlines={hlines}...") + try: + start = time_ns() + display.clear(hlines=hlines, color=color565(*rgb)) + end = time_ns() + print(f"Display cleared in {end - start} ms.") + except Exception as e: + print(e) + sleep(1) + + sleep(5) diff --git a/ili9341.py b/ili9341.py new file mode 100644 index 0000000..fee67c5 --- /dev/null +++ b/ili9341.py @@ -0,0 +1,1088 @@ +"""ILI9341 LCD/Touch module.""" +from time import sleep +from math import cos, sin, pi, radians +from sys import implementation +from framebuf import FrameBuffer, RGB565 # type: ignore +from micropython import const # type: ignore + + +def color565(r, g, b): + """Return RGB565 color value. + + Args: + r (int): Red value. + g (int): Green value. + b (int): Blue value. + """ + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +class Display(object): + """Serial interface for 16-bit color (5-6-5 RGB) IL9341 display. + + Note: All coordinates are zero based. + """ + + # Command constants from ILI9341 datasheet + NOP = const(0x00) # No-op + SWRESET = const(0x01) # Software reset + RDDID = const(0x04) # Read display ID info + RDDST = const(0x09) # Read display status + SLPIN = const(0x10) # Enter sleep mode + SLPOUT = const(0x11) # Exit sleep mode + PTLON = const(0x12) # Partial mode on + NORON = const(0x13) # Normal display mode on + RDMODE = const(0x0A) # Read display power mode + RDMADCTL = const(0x0B) # Read display MADCTL + RDPIXFMT = const(0x0C) # Read display pixel format + RDIMGFMT = const(0x0D) # Read display image format + RDSELFDIAG = const(0x0F) # Read display self-diagnostic + INVOFF = const(0x20) # Display inversion off + INVON = const(0x21) # Display inversion on + GAMMASET = const(0x26) # Gamma set + DISPLAY_OFF = const(0x28) # Display off + DISPLAY_ON = const(0x29) # Display on + SET_COLUMN = const(0x2A) # Column address set + SET_PAGE = const(0x2B) # Page address set + WRITE_RAM = const(0x2C) # Memory write + READ_RAM = const(0x2E) # Memory read + PTLAR = const(0x30) # Partial area + VSCRDEF = const(0x33) # Vertical scrolling definition + MADCTL = const(0x36) # Memory access control + VSCRSADD = const(0x37) # Vertical scrolling start address + PIXFMT = const(0x3A) # COLMOD: Pixel format set + WRITE_DISPLAY_BRIGHTNESS = const(0x51) # Brightness hardware dependent! + READ_DISPLAY_BRIGHTNESS = const(0x52) + WRITE_CTRL_DISPLAY = const(0x53) + READ_CTRL_DISPLAY = const(0x54) + WRITE_CABC = const(0x55) # Write Content Adaptive Brightness Control + READ_CABC = const(0x56) # Read Content Adaptive Brightness Control + WRITE_CABC_MINIMUM = const(0x5E) # Write CABC Minimum Brightness + READ_CABC_MINIMUM = const(0x5F) # Read CABC Minimum Brightness + FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors) + FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors) + FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors) + INVCTR = const(0xB4) # Display inversion control + DFUNCTR = const(0xB6) # Display function control + PWCTR1 = const(0xC0) # Power control 1 + PWCTR2 = const(0xC1) # Power control 2 + PWCTRA = const(0xCB) # Power control A + PWCTRB = const(0xCF) # Power control B + VMCTR1 = const(0xC5) # VCOM control 1 + VMCTR2 = const(0xC7) # VCOM control 2 + RDID1 = const(0xDA) # Read ID 1 + RDID2 = const(0xDB) # Read ID 2 + RDID3 = const(0xDC) # Read ID 3 + RDID4 = const(0xDD) # Read ID 4 + GMCTRP1 = const(0xE0) # Positive gamma correction + GMCTRN1 = const(0xE1) # Negative gamma correction + DTCA = const(0xE8) # Driver timing control A + DTCB = const(0xEA) # Driver timing control B + POSC = const(0xED) # Power on sequence control + ENABLE3G = const(0xF2) # Enable 3 gamma control + PUMPRC = const(0xF7) # Pump ratio control + + MIRROR_ROTATE = { # MADCTL configurations for rotation and mirroring + (False, 0): 0x80, # 1000 0000 + (False, 90): 0xE0, # 1110 0000 + (False, 180): 0x40, # 0100 0000 + (False, 270): 0x20, # 0010 0000 + (True, 0): 0xC0, # 1100 0000 + (True, 90): 0x60, # 0110 0000 + (True, 180): 0x00, # 0000 0000 + (True, 270): 0xA0 # 1010 0000 + } + + def __init__(self, spi, cs, dc, rst, width=240, height=320, rotation=0, + mirror=False, bgr=True, gamma=True, x_offset=0, y_offset=0): + """Initialize OLED. + + Args: + spi (Class Spi): SPI interface for OLED + cs (Class Pin): Chip select pin + dc (Class Pin): Data/Command pin + rst (Class Pin): Reset pin + width (Optional int): Screen width (default 240) + height (Optional int): Screen height (default 320) + rotation (Optional int): Rotation must be 0 default, 90. 180 or 270 + mirror (Optional bool): Mirror display (default False) + bgr (Optional bool): Swaps red and blue colors (default True) + gamma (Optional bool): Custom gamma correction (default True) + x_offset (Optional int): X-axis origin offset (default 0) + y_offset (Optional int): Y-axis origin offset (default 0) + """ + self.spi = spi + self.cs = cs + self.dc = dc + self.rst = rst + self.width = width + self.height = height + if (mirror, rotation) not in self.MIRROR_ROTATE: + raise ValueError('Rotation must be 0, 90, 180 or 270.') + else: + self.rotation = self.MIRROR_ROTATE[mirror, rotation] + if bgr: # Set BGR bit + self.rotation |= 0b00001000 + # Check for offset + self.offset = bool(x_offset or y_offset) + self.x_offset = x_offset + self.y_offset = y_offset + + # Initialize GPIO pins and set implementation specific methods + if implementation.name == 'circuitpython': + self.cs.switch_to_output(value=True) + self.dc.switch_to_output(value=False) + self.rst.switch_to_output(value=True) + self.reset = self.reset_cpy + self.write_cmd = self.write_cmd_cpy + self.write_data = self.write_data_cpy + else: + self.cs.init(self.cs.OUT, value=1) + self.dc.init(self.dc.OUT, value=0) + self.rst.init(self.rst.OUT, value=1) + self.reset = self.reset_mpy + self.write_cmd = self.write_cmd_mpy + self.write_data = self.write_data_mpy + self.reset() + # Send initialization commands + self.write_cmd(self.SWRESET) # Software reset + sleep(.1) + self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B + self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl + self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A + self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A + self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control + self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B + self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1 + self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2 + self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1 + self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2 + self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl + self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address + self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format + self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl + self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27) + self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl + self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected + if gamma: # Use custom gamma correction values + self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, + 0x00) + self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, + 0x0F) + self.write_cmd(self.SLPOUT) # Exit sleep + sleep(.1) + self.write_cmd(self.DISPLAY_ON) # Display on + sleep(.1) + self.clear() + + def block(self, x0, y0, x1, y1, data): + """Write a block of data to display. + + Args: + x0 (int): Starting X position. + y0 (int): Starting Y position. + x1 (int): Ending X position. + y1 (int): Ending Y position. + data (bytes): Data buffer to write. + """ + if self.offset: # Add offset if specified + x0 += self.x_offset + x1 += self.x_offset + y0 += self.y_offset + y1 += self.y_offset + + self.write_cmd(self.SET_COLUMN, + x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff) + self.write_cmd(self.SET_PAGE, + y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff) + self.write_cmd(self.WRITE_RAM) + self.write_data(data) + + def cleanup(self): + """Clean up resources.""" + self.clear() + self.display_off() + self.spi.deinit() + print('display off') + + def clear(self, color=0, hlines=8): + """Clear display. + + Args: + color (Optional int): RGB565 color value (Default: 0 = Black). + hlines (Optional int): # of horizontal lines per chunk (Default: 8) + Note: + hlines was introduced to deal with memory allocation on some + boards. Smaller values allocate less memory but take longer + to execute. hlines must be a factor of the display height. + For example, for a 240 pixel height, valid values for hline + would be 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 30, 40, etc. + Higher values may result in memory allocation errors. + """ + w = self.width + h = self.height + assert hlines > 0 and h % hlines == 0, ( + "hlines must be a non-zero factor of height.") + # Clear display + if color: + line = color.to_bytes(2, 'big') * (w * hlines) + else: + line = bytearray(w * 2 * hlines) + for y in range(0, h, hlines): + self.block(0, y, w - 1, y + hlines - 1, line) + + def display_off(self): + """Turn display off.""" + self.write_cmd(self.DISPLAY_OFF) + + def display_on(self): + """Turn display on.""" + self.write_cmd(self.DISPLAY_ON) + + def draw_circle(self, x0, y0, r, color): + """Draw a circle. + + Args: + x0 (int): X coordinate of center point. + y0 (int): Y coordinate of center point. + r (int): Radius. + color (int): RGB565 color value. + """ + f = 1 - r + dx = 1 + dy = -r - r + x = 0 + y = r + self.draw_pixel(x0, y0 + r, color) + self.draw_pixel(x0, y0 - r, color) + self.draw_pixel(x0 + r, y0, color) + self.draw_pixel(x0 - r, y0, color) + while x < y: + if f >= 0: + y -= 1 + dy += 2 + f += dy + x += 1 + dx += 2 + f += dx + self.draw_pixel(x0 + x, y0 + y, color) + self.draw_pixel(x0 - x, y0 + y, color) + self.draw_pixel(x0 + x, y0 - y, color) + self.draw_pixel(x0 - x, y0 - y, color) + self.draw_pixel(x0 + y, y0 + x, color) + self.draw_pixel(x0 - y, y0 + x, color) + self.draw_pixel(x0 + y, y0 - x, color) + self.draw_pixel(x0 - y, y0 - x, color) + + def draw_ellipse(self, x0, y0, a, b, color): + """Draw an ellipse. + + Args: + x0, y0 (int): Coordinates of center point. + a (int): Semi axis horizontal. + b (int): Semi axis vertical. + color (int): RGB565 color value. + Note: + The center point is the center of the x0,y0 pixel. + Since pixels are not divisible, the axes are integer rounded + up to complete on a full pixel. Therefore the major and + minor axes are increased by 1. + """ + a2 = a * a + b2 = b * b + twoa2 = a2 + a2 + twob2 = b2 + b2 + x = 0 + y = b + px = 0 + py = twoa2 * y + # Plot initial points + self.draw_pixel(x0 + x, y0 + y, color) + self.draw_pixel(x0 - x, y0 + y, color) + self.draw_pixel(x0 + x, y0 - y, color) + self.draw_pixel(x0 - x, y0 - y, color) + # Region 1 + p = round(b2 - (a2 * b) + (0.25 * a2)) + while px < py: + x += 1 + px += twob2 + if p < 0: + p += b2 + px + else: + y -= 1 + py -= twoa2 + p += b2 + px - py + self.draw_pixel(x0 + x, y0 + y, color) + self.draw_pixel(x0 - x, y0 + y, color) + self.draw_pixel(x0 + x, y0 - y, color) + self.draw_pixel(x0 - x, y0 - y, color) + # Region 2 + p = round(b2 * (x + 0.5) * (x + 0.5) + + a2 * (y - 1) * (y - 1) - a2 * b2) + while y > 0: + y -= 1 + py -= twoa2 + if p > 0: + p += a2 - py + else: + x += 1 + px += twob2 + p += a2 - py + px + self.draw_pixel(x0 + x, y0 + y, color) + self.draw_pixel(x0 - x, y0 + y, color) + self.draw_pixel(x0 + x, y0 - y, color) + self.draw_pixel(x0 - x, y0 - y, color) + + def draw_hline(self, x, y, w, color): + """Draw a horizontal line. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of line. + color (int): RGB565 color value. + """ + if self.is_off_grid(x, y, x + w - 1, y): + return + line = color.to_bytes(2, 'big') * w + self.block(x, y, x + w - 1, y, line) + + def draw_image(self, path, x=0, y=0, w=320, h=240): + """Draw image from flash. + + Args: + path (string): Image file path. + x (int): X coordinate of image left. Default is 0. + y (int): Y coordinate of image top. Default is 0. + w (int): Width of image. Default is 320. + h (int): Height of image. Default is 240. + """ + x2 = x + w - 1 + y2 = y + h - 1 + if self.is_off_grid(x, y, x2, y2): + return + with open(path, "rb") as f: + chunk_height = 1024 // w + chunk_count, remainder = divmod(h, chunk_height) + chunk_size = chunk_height * w * 2 + chunk_y = y + if chunk_count: + for c in range(0, chunk_count): + buf = f.read(chunk_size) + self.block(x, chunk_y, + x2, chunk_y + chunk_height - 1, + buf) + chunk_y += chunk_height + if remainder: + buf = f.read(remainder * w * 2) + self.block(x, chunk_y, + x2, chunk_y + remainder - 1, + buf) + + def draw_letter(self, x, y, letter, font, color, background=0, + landscape=False, rotate_180=False): + """Draw a letter. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + letter (string): Letter to draw. + font (XglcdFont object): Font. + color (int): RGB565 color value. + background (int): RGB565 background color (default: black) + landscape (bool): Orientation (default: False = portrait) + rotate_180 (bool): Rotate text by 180 degrees + """ + buf, w, h = font.get_letter(letter, color, background, landscape) + if rotate_180: + # Manually rotate the buffer by 180 degrees + # ensure bytes pairs for each pixel retain color565 + new_buf = bytearray(len(buf)) + num_pixels = len(buf) // 2 + for i in range(num_pixels): + # The index for the new buffer's byte pair + new_idx = (num_pixels - 1 - i) * 2 + # The index for the original buffer's byte pair + old_idx = i * 2 + # Swap the pixels + new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1] + buf = new_buf + + # Check for errors (Font could be missing specified letter) + if w == 0: + return w, h + + if landscape: + y -= w + if self.is_off_grid(x, y, x + h - 1, y + w - 1): + return 0, 0 + self.block(x, y, + x + h - 1, y + w - 1, + buf) + else: + if self.is_off_grid(x, y, x + w - 1, y + h - 1): + return 0, 0 + self.block(x, y, + x + w - 1, y + h - 1, + buf) + return w, h + + def draw_line(self, x1, y1, x2, y2, color): + """Draw a line using Bresenham's algorithm. + + Args: + x1, y1 (int): Starting coordinates of the line + x2, y2 (int): Ending coordinates of the line + color (int): RGB565 color value. + """ + # Check for horizontal line + if y1 == y2: + if x1 > x2: + x1, x2 = x2, x1 + self.draw_hline(x1, y1, x2 - x1 + 1, color) + return + # Check for vertical line + if x1 == x2: + if y1 > y2: + y1, y2 = y2, y1 + self.draw_vline(x1, y1, y2 - y1 + 1, color) + return + # Confirm coordinates in boundary + if self.is_off_grid(min(x1, x2), min(y1, y2), + max(x1, x2), max(y1, y2)): + return + # Changes in x, y + dx = x2 - x1 + dy = y2 - y1 + # Determine how steep the line is + is_steep = abs(dy) > abs(dx) + # Rotate line + if is_steep: + x1, y1 = y1, x1 + x2, y2 = y2, x2 + # Swap start and end points if necessary + if x1 > x2: + x1, x2 = x2, x1 + y1, y2 = y2, y1 + # Recalculate differentials + dx = x2 - x1 + dy = y2 - y1 + # Calculate error + error = dx >> 1 + ystep = 1 if y1 < y2 else -1 + y = y1 + for x in range(x1, x2 + 1): + # Had to reverse HW ???? + if not is_steep: + self.draw_pixel(x, y, color) + else: + self.draw_pixel(y, x, color) + error -= abs(dy) + if error < 0: + y += ystep + error += dx + + def draw_lines(self, coords, color): + """Draw multiple lines. + + Args: + coords ([[int, int],...]): Line coordinate X, Y pairs + color (int): RGB565 color value. + """ + # Starting point + x1, y1 = coords[0] + # Iterate through coordinates + for i in range(1, len(coords)): + x2, y2 = coords[i] + self.draw_line(x1, y1, x2, y2, color) + x1, y1 = x2, y2 + + def draw_pixel(self, x, y, color): + """Draw a single pixel. + + Args: + x (int): X position. + y (int): Y position. + color (int): RGB565 color value. + """ + if self.is_off_grid(x, y, x, y): + return + self.block(x, y, x, y, color.to_bytes(2, 'big')) + + def draw_polygon(self, sides, x0, y0, r, color, rotate=0): + """Draw an n-sided regular polygon. + + Args: + sides (int): Number of polygon sides. + x0, y0 (int): Coordinates of center point. + r (int): Radius. + color (int): RGB565 color value. + rotate (Optional float): Rotation in degrees relative to origin. + Note: + The center point is the center of the x0,y0 pixel. + Since pixels are not divisible, the radius is integer rounded + up to complete on a full pixel. Therefore diameter = 2 x r + 1. + """ + coords = [] + theta = radians(rotate) + n = sides + 1 + for s in range(n): + t = 2.0 * pi * s / sides + theta + coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) + + # Cast to python float first to fix rounding errors + self.draw_lines(coords, color=color) + + def draw_rectangle(self, x, y, w, h, color): + """Draw a rectangle. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of rectangle. + h (int): Height of rectangle. + color (int): RGB565 color value. + """ + x2 = x + w - 1 + y2 = y + h - 1 + self.draw_hline(x, y, w, color) + self.draw_hline(x, y2, w, color) + self.draw_vline(x, y, h, color) + self.draw_vline(x2, y, h, color) + + def draw_sprite(self, buf, x, y, w, h): + """Draw a sprite (optimized for horizontal drawing). + + Args: + buf (bytearray): Buffer to draw. + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of drawing. + h (int): Height of drawing. + """ + x2 = x + w - 1 + y2 = y + h - 1 + if self.is_off_grid(x, y, x2, y2): + return + self.block(x, y, x2, y2, buf) + + def draw_text(self, x, y, text, font, color, background=0, + landscape=False, rotate_180=False, spacing=1): + """Draw text. + + Args: + x (int): Starting X position + y (int): Starting Y position + text (string): Text to draw + font (XglcdFont object): Font + color (int): RGB565 color value + background (int): RGB565 background color (default: black) + landscape (bool): Orientation (default: False = portrait) + rotate_180 (bool): Rotate text by 180 degrees + spacing (int): Pixels between letters (default: 1) + """ + iterable_text = reversed(text) if rotate_180 else text + for letter in iterable_text: + # Get letter array and letter dimensions + w, h = self.draw_letter(x, y, letter, font, color, background, + landscape, rotate_180) + # Stop on error + if w == 0 or h == 0: + print('Invalid width {0} or height {1}'.format(w, h)) + return + + if landscape: + # Fill in spacing + if spacing: + self.fill_hrect(x, y - w - spacing, h, spacing, background) + # Position y for next letter + y -= (w + spacing) + else: + # Fill in spacing + if spacing: + self.fill_hrect(x + w, y, spacing, h, background) + # Position x for next letter + x += (w + spacing) + + # # Fill in spacing + # if spacing: + # self.fill_vrect(x + w, y, spacing, h, background) + # # Position x for next letter + # x += w + spacing + + def draw_text8x8(self, x, y, text, color, background=0, + rotate=0): + """Draw text using built-in MicroPython 8x8 bit font. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + text (string): Text to draw. + color (int): RGB565 color value. + background (int): RGB565 background color (default: black). + rotate(int): 0, 90, 180, 270 + """ + w = len(text) * 8 + h = 8 + # Confirm coordinates in boundary + if self.is_off_grid(x, y, x + 7, y + 7): + return + buf = bytearray(w * 16) + fbuf = FrameBuffer(buf, w, h, RGB565) + if background != 0: + # Swap background color bytes to correct for framebuf endianness + b_color = ((background & 0xFF) << 8) | ((background & 0xFF00) >> 8) + fbuf.fill(b_color) + # Swap text color bytes to correct for framebuf endianness + t_color = ((color & 0xFF) << 8) | ((color & 0xFF00) >> 8) + fbuf.text(text, 0, 0, t_color) + if rotate == 0: + self.block(x, y, x + w - 1, y + (h - 1), buf) + elif rotate == 90: + buf2 = bytearray(w * 16) + fbuf2 = FrameBuffer(buf2, h, w, RGB565) + for y1 in range(h): + for x1 in range(w): + fbuf2.pixel(y1, x1, + fbuf.pixel(x1, (h - 1) - y1)) + self.block(x, y, x + (h - 1), y + w - 1, buf2) + elif rotate == 180: + buf2 = bytearray(w * 16) + fbuf2 = FrameBuffer(buf2, w, h, RGB565) + for y1 in range(h): + for x1 in range(w): + fbuf2.pixel(x1, y1, + fbuf.pixel((w - 1) - x1, (h - 1) - y1)) + self.block(x, y, x + w - 1, y + (h - 1), buf2) + elif rotate == 270: + buf2 = bytearray(w * 16) + fbuf2 = FrameBuffer(buf2, h, w, RGB565) + for y1 in range(h): + for x1 in range(w): + fbuf2.pixel(y1, x1, + fbuf.pixel((w - 1) - x1, y1)) + self.block(x, y, x + (h - 1), y + w - 1, buf2) + + def draw_vline(self, x, y, h, color): + """Draw a vertical line. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + h (int): Height of line. + color (int): RGB565 color value. + """ + # Confirm coordinates in boundary + if self.is_off_grid(x, y, x, y + h - 1): + return + line = color.to_bytes(2, 'big') * h + self.block(x, y, x, y + h - 1, line) + + def fill_circle(self, x0, y0, r, color): + """Draw a filled circle. + + Args: + x0 (int): X coordinate of center point. + y0 (int): Y coordinate of center point. + r (int): Radius. + color (int): RGB565 color value. + """ + f = 1 - r + dx = 1 + dy = -r - r + x = 0 + y = r + self.draw_vline(x0, y0 - r, 2 * r + 1, color) + while x < y: + if f >= 0: + y -= 1 + dy += 2 + f += dy + x += 1 + dx += 2 + f += dx + self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color) + self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color) + self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color) + self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color) + + def fill_ellipse(self, x0, y0, a, b, color): + """Draw a filled ellipse. + + Args: + x0, y0 (int): Coordinates of center point. + a (int): Semi axis horizontal. + b (int): Semi axis vertical. + color (int): RGB565 color value. + Note: + The center point is the center of the x0,y0 pixel. + Since pixels are not divisible, the axes are integer rounded + up to complete on a full pixel. Therefore the major and + minor axes are increased by 1. + """ + a2 = a * a + b2 = b * b + twoa2 = a2 + a2 + twob2 = b2 + b2 + x = 0 + y = b + px = 0 + py = twoa2 * y + # Plot initial points + self.draw_line(x0, y0 - y, x0, y0 + y, color) + # Region 1 + p = round(b2 - (a2 * b) + (0.25 * a2)) + while px < py: + x += 1 + px += twob2 + if p < 0: + p += b2 + px + else: + y -= 1 + py -= twoa2 + p += b2 + px - py + self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) + self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) + # Region 2 + p = round(b2 * (x + 0.5) * (x + 0.5) + + a2 * (y - 1) * (y - 1) - a2 * b2) + while y > 0: + y -= 1 + py -= twoa2 + if p > 0: + p += a2 - py + else: + x += 1 + px += twob2 + p += a2 - py + px + self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) + self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) + + def fill_hrect(self, x, y, w, h, color): + """Draw a filled rectangle (optimized for horizontal drawing). + + Args: + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of rectangle. + h (int): Height of rectangle. + color (int): RGB565 color value. + """ + if self.is_off_grid(x, y, x + w - 1, y + h - 1): + return + chunk_height = 1024 // w + chunk_count, remainder = divmod(h, chunk_height) + chunk_size = chunk_height * w + chunk_y = y + if chunk_count: + buf = color.to_bytes(2, 'big') * chunk_size + for c in range(0, chunk_count): + self.block(x, chunk_y, + x + w - 1, chunk_y + chunk_height - 1, + buf) + chunk_y += chunk_height + + if remainder: + buf = color.to_bytes(2, 'big') * remainder * w + self.block(x, chunk_y, + x + w - 1, chunk_y + remainder - 1, + buf) + + def fill_rectangle(self, x, y, w, h, color): + """Draw a filled rectangle. + + Args: + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of rectangle. + h (int): Height of rectangle. + color (int): RGB565 color value. + """ + if self.is_off_grid(x, y, x + w - 1, y + h - 1): + return + if w > h: + self.fill_hrect(x, y, w, h, color) + else: + self.fill_vrect(x, y, w, h, color) + + def fill_polygon(self, sides, x0, y0, r, color, rotate=0): + """Draw a filled n-sided regular polygon. + + Args: + sides (int): Number of polygon sides. + x0, y0 (int): Coordinates of center point. + r (int): Radius. + color (int): RGB565 color value. + rotate (Optional float): Rotation in degrees relative to origin. + Note: + The center point is the center of the x0,y0 pixel. + Since pixels are not divisible, the radius is integer rounded + up to complete on a full pixel. Therefore diameter = 2 x r + 1. + """ + # Determine side coordinates + coords = [] + theta = radians(rotate) + n = sides + 1 + for s in range(n): + t = 2.0 * pi * s / sides + theta + coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) + # Starting point + x1, y1 = coords[0] + # Minimum Maximum X dict + xdict = {y1: [x1, x1]} + # Iterate through coordinates + for row in coords[1:]: + x2, y2 = row + xprev, yprev = x2, y2 + # Calculate perimeter + # Check for horizontal side + if y1 == y2: + if x1 > x2: + x1, x2 = x2, x1 + if y1 in xdict: + xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])] + else: + xdict[y1] = [x1, x2] + x1, y1 = xprev, yprev + continue + # Non horizontal side + # Changes in x, y + dx = x2 - x1 + dy = y2 - y1 + # Determine how steep the line is + is_steep = abs(dy) > abs(dx) + # Rotate line + if is_steep: + x1, y1 = y1, x1 + x2, y2 = y2, x2 + # Swap start and end points if necessary + if x1 > x2: + x1, x2 = x2, x1 + y1, y2 = y2, y1 + # Recalculate differentials + dx = x2 - x1 + dy = y2 - y1 + # Calculate error + error = dx >> 1 + ystep = 1 if y1 < y2 else -1 + y = y1 + # Calcualte minimum and maximum x values + for x in range(x1, x2 + 1): + if is_steep: + if x in xdict: + xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])] + else: + xdict[x] = [y, y] + else: + if y in xdict: + xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])] + else: + xdict[y] = [x, x] + error -= abs(dy) + if error < 0: + y += ystep + error += dx + x1, y1 = xprev, yprev + # Fill polygon + for y, x in xdict.items(): + self.draw_hline(x[0], y, x[1] - x[0] + 2, color) + + def fill_vrect(self, x, y, w, h, color): + """Draw a filled rectangle (optimized for vertical drawing). + + Args: + x (int): Starting X position. + y (int): Starting Y position. + w (int): Width of rectangle. + h (int): Height of rectangle. + color (int): RGB565 color value. + """ + if self.is_off_grid(x, y, x + w - 1, y + h - 1): + return + chunk_width = 1024 // h + chunk_count, remainder = divmod(w, chunk_width) + chunk_size = chunk_width * h + chunk_x = x + if chunk_count: + buf = color.to_bytes(2, 'big') * chunk_size + for c in range(0, chunk_count): + self.block(chunk_x, y, + chunk_x + chunk_width - 1, y + h - 1, + buf) + chunk_x += chunk_width + + if remainder: + buf = color.to_bytes(2, 'big') * remainder * h + self.block(chunk_x, y, + chunk_x + remainder - 1, y + h - 1, + buf) + + def invert(self, enable=True): + """Enables or disables inversion of display colors. + + Args: + enable (Optional bool): True=enable, False=disable + """ + if enable: + self.write_cmd(self.INVON) + else: + self.write_cmd(self.INVOFF) + + def is_off_grid(self, xmin, ymin, xmax, ymax): + """Check if coordinates extend past display boundaries. + + Args: + xmin (int): Minimum horizontal pixel. + ymin (int): Minimum vertical pixel. + xmax (int): Maximum horizontal pixel. + ymax (int): Maximum vertical pixel. + Returns: + boolean: False = Coordinates OK, True = Error. + """ + if xmin < 0: + print('x-coordinate: {0} below minimum of 0.'.format(xmin)) + return True + if ymin < 0: + print('y-coordinate: {0} below minimum of 0.'.format(ymin)) + return True + if xmax >= self.width: + print('x-coordinate: {0} above maximum of {1}.'.format( + xmax, self.width - 1)) + return True + if ymax >= self.height: + print('y-coordinate: {0} above maximum of {1}.'.format( + ymax, self.height - 1)) + return True + return False + + def load_sprite(self, path, w, h): + """Load sprite image. + + Args: + path (string): Image file path. + w (int): Width of image. + h (int): Height of image. + Notes: + w x h cannot exceed 2048 on boards w/o PSRAM + """ + buf_size = w * h * 2 + with open(path, "rb") as f: + return f.read(buf_size) + + def reset_cpy(self): + """Perform reset: Low=initialization, High=normal operation. + + Notes: CircuitPython implemntation + """ + self.rst.value = False + sleep(.05) + self.rst.value = True + sleep(.05) + + def reset_mpy(self): + """Perform reset: Low=initialization, High=normal operation. + + Notes: MicroPython implemntation + """ + self.rst(0) + sleep(.05) + self.rst(1) + sleep(.05) + + def scroll(self, y): + """Scroll display vertically. + + Args: + y (int): Number of pixels to scroll display. + """ + self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF) + + def set_scroll(self, top, bottom): + """Set the height of the top and bottom scroll margins. + + Args: + top (int): Height of top scroll margin + bottom (int): Height of bottom scroll margin + """ + if top + bottom <= self.height: + middle = self.height - (top + bottom) + self.write_cmd(self.VSCRDEF, + top >> 8, + top & 0xFF, + middle >> 8, + middle & 0xFF, + bottom >> 8, + bottom & 0xFF) + + def sleep(self, enable=True): + """Enters or exits sleep mode. + + Args: + enable (bool): True (default)=Enter sleep mode, False=Exit sleep + """ + if enable: + self.write_cmd(self.SLPIN) + else: + self.write_cmd(self.SLPOUT) + + def write_cmd_mpy(self, command, *args): + """Write command to OLED (MicroPython). + + Args: + command (byte): ILI9341 command code. + *args (optional bytes): Data to transmit. + """ + self.dc(0) + self.cs(0) + self.spi.write(bytearray([command])) + self.cs(1) + # Handle any passed data + if len(args) > 0: + self.write_data(bytearray(args)) + + def write_cmd_cpy(self, command, *args): + """Write command to OLED (CircuitPython). + + Args: + command (byte): ILI9341 command code. + *args (optional bytes): Data to transmit. + """ + self.dc.value = False + self.cs.value = False + # Confirm SPI locked before writing + while not self.spi.try_lock(): + pass + self.spi.write(bytearray([command])) + self.spi.unlock() + self.cs.value = True + # Handle any passed data + if len(args) > 0: + self.write_data(bytearray(args)) + + def write_data_mpy(self, data): + """Write data to OLED (MicroPython). + + Args: + data (bytes): Data to transmit. + """ + self.dc(1) + self.cs(0) + self.spi.write(data) + self.cs(1) + + def write_data_cpy(self, data): + """Write data to OLED (CircuitPython). + + Args: + data (bytes): Data to transmit. + """ + self.dc.value = True + self.cs.value = False + # Confirm SPI locked before writing + while not self.spi.try_lock(): + pass + self.spi.write(data) + self.spi.unlock() + self.cs.value = True diff --git a/led_program.py b/led_program.py new file mode 100644 index 0000000..db10b9a --- /dev/null +++ b/led_program.py @@ -0,0 +1,13 @@ +import time + +from machine import Pin + +led_pin = Pin(32, Pin.OUT) + + +def boostrap(): + while True: + led_pin.on() + time.sleep(0.5) + led_pin.off() + time.sleep(0.5) diff --git a/main.py b/main.py new file mode 100644 index 0000000..26a4b3d --- /dev/null +++ b/main.py @@ -0,0 +1,13 @@ +import _thread + +from machine import soft_reset + +# display +import display_program + +# MODULES +# led program +import led_program + +_thread.start_new_thread(led_program.boostrap, ()) +_thread.start_new_thread(display_program.display, ())