def get_matrix(self): if self.data_cache is None: self.make() if not self.border: return self.modules width = len(self.modules) + self.border * 2 buf = bytearray(width * (width + 7) // 8) fb = FrameBuffer(buf, width, width, MONO_HLSB) fb.fill(0) y = self.border for module in self.modules: x = self.border for p in module: fb.pixel(x, y, p) x += 1 y += 1 return (fb, width)
class ILI9341: def __init__(self, spi, cs=14, dc=27, rst=33, bl=32): self.buffer = bytearray(32) self.letter = FrameBuffer(bytearray(8), 8, 8, MONO_HLSB) self.spi = spi self.cs = Pin(cs, Pin.OUT) self.dc = Pin(dc, Pin.OUT) self.rst = Pin(rst, Pin.OUT) self.bl = Pin(bl, Pin.OUT) self.width = 320 self.height = 240 self.char_width = 16 self.char_height = 16 self.offset = 0 self.background = color565(0, 0, 0) self._reset() self._setup() def _reset(self): self.cs.value(1) self.dc.value(0) self.rst.value(0) utime.sleep_ms(50) self.rst.value(1) utime.sleep_ms(50) def _setup(self): for command, arguments in ( (0xef, b'\x03\x80\x02'), (0xcf, b'\x00\xc1\x30'), (0xed, b'\x64\x03\x12\x81'), (0xe8, b'\x85\x00\x78'), (0xcb, b'\x39\x2c\x00\x34\x02'), (0xf7, b'\x20'), (0xea, b'\x00\x00'), (0xc0, b'\x23'), # Power Control 1, VRH[5:0] (0xc1, b'\x10'), # Power Control 2, SAP[2:0], BT[3:0] (0xc5, b'\x3e\x28'), # VCM Control 1 (0xc7, b'\x86'), # VCM Control 2 (0x36, b'\x48'), # Memory Access Control (0x3a, b'\x55'), # Pixel Format (0xb1, b'\x00\x18'), # FRMCTR1 (0xb6, b'\x08\x82\x27'), # Display Function Control (0xf2, b'\x00'), # Gamma Function Disable (0x26, b'\x01'), # Gamma Curve Selected (0xe0, b'\x0f\x31\x2b\x0c\x0e\x08\x4e\xf1\x37\x07\x10\x03\x0e\x09\x00' ), # Set Gamma (0xe1, b'\x00\x0e\x14\x03\x11\x07\x31\xc1\x48\x08\x0f\x0c\x31\x36\x0f')): self._write_command(command, arguments) self._write_command(_CMD_WAKE) utime.sleep_ms(120) def on(self): self._write_command(_CMD_DISPLAY_ON) self.bl.value(1) def off(self): self.bl.value(0) self._write_command(_CMD_DISPLAY_OFF) def set_inversion(self, inverse): if inverse: self._write_command(_CMD_DISPLAY_INVERSION_ON) else: self._write_command(_CMD_DISPLAY_INVERSION_OFF) def to_color(self, r, g, b): return color565(r, g, b) def set_background(self, background=color565(0, 0, 0)): self.fill_rectangle(0, 0, self.width - 1, self.height - 1, background) self.background = background def set_pixel(self, x, y, color=color565(255, 255, 255)): if x >= 0 and x < self.width and y >= 0 and y < self.height: self.fill_rectangle(x, y, x, y, color) def draw_line(self, x0, y0, x1, y1, color=color565(255, 255, 255)): dx = abs(x1 - x0) dy = -abs(y1 - y0) sx = 1 if x0 < x1 else -1 sy = 1 if y0 < y1 else -1 e = dx + dy while x0 != x1 or y0 != y1: self.set_pixel(x0, y0, color) e2 = e << 1 if e2 > dy: e += dy x0 += sx if e2 < dx: e += dx y0 += sy def draw_polyline(self, x, y, points, color=color565(255, 255, 255)): last_point = None for point in points: if last_point is not None: self.draw_line(x + last_point[0], y + last_point[1], x + point[0], y + point[1], color) last_point = point def draw_string(self, x_origin, y_origin, chars, color=color565(255, 255, 255)): for char in chars: self.letter.fill(0) self.letter.text(char, 0, 0) for x in range(0, 8): for y in range(0, 8): x0 = x_origin + (x << 1) y0 = y_origin + (y << 1) x1 = x0 + 2 y1 = y0 + 2 self.fill_rectangle( x0, y0, x1, y1, color if self.letter.pixel(x, y) > 0 else self.background) x_origin += 16 def rotate_up(self, delta=1): self.offset = (self.offset + delta) % self.height self._write_command(_CMD_LINE_SET, ustruct.pack('>H', self.offset)) def scroll_up(self, delta): self.fill_rectangle(0, 0, self.width - 1, delta, color=self.background) self.rotate_up(delta) def fill_rectangle(self, x0, y0, x1, y1, color): x0, x1 = self.width - x1 - 1, self.width - x0 - 1 if x0 < 0 and x1 < 0: return if x0 >= self.width and x1 >= self.width: return if y0 < 0 and y1 < 0: return if y0 >= self.height and y1 >= self.height: return if x0 < 0: x0 = 0 if x0 >= self.width: x0 = self.width - 1 if y0 < 0: y0 = 0 if y0 >= self.height: y0 = self.height - 1 if x1 < 0: x1 = 0 if x1 >= self.width: x1 = self.width - 1 if y1 < 0: y1 = 0 if y1 >= self.height: y1 = self.height - 1 w = x1 - x0 + 1 h = y1 - y0 + 1 pixel_count = min(16, w * h) color_msb = color >> 8 color_lsb = color & 255 position = 0 for index in range(0, pixel_count): self.buffer[position] = color_msb position += 1 self.buffer[position] = color_lsb position += 1 if pixel_count == 16: self._fill_large_rectangle(x0, y0, x1, y1) else: self._fill_small_rectangle(x0, y0, x1, y1, position) def _fill_large_rectangle(self, x0, y0, x1, y1): y = y0 while y <= y1: x = x0 while x <= x1: x_right = min(x1, x + 15) self._fill_small_rectangle(x, y, x_right, y, (x_right - x + 1) << 1) x = x_right + 1 y += 1 def _fill_small_rectangle(self, x0, y0, x1, y1, position): y0 += self.offset y1 += self.offset y0 %= self.height y1 %= self.height self._write_command(_CMD_COLUMN_SET, ustruct.pack(">HH", x0, x1)) self._write_command(_CMD_PAGE_SET, ustruct.pack(">HH", y0, y1)) self._write_command(_CMD_RAM_WRITE, memoryview(self.buffer)[0:position]) def _write_command(self, command, arguments=None): self.dc.value(0) self.cs.value(0) self.spi.write(bytearray([command])) self.cs.value(1) if arguments is not None: self.dc.value(1) self.cs.value(0) self.spi.write(arguments) self.cs.value(1)
class Screen: def __init__(self): self.textbuffer = TextBuffer(cols, rows) # make the framebuffer we draw into the size of one line of text as that's all we need self.buf = bytearray(screen_width * font_height // 8) # the screen defaults to portrait and we want to use it in landscape so we have to rotate as we go, unfortunately. that's why dimensions look swapped around self.fb = FrameBuffer(self.buf, font_height, screen_width, MONO_HLSB) sck = Pin(18, Pin.OUT) mosi = Pin(23, Pin.OUT) miso = Pin(19, Pin.IN) spi = SPI(2, baudrate=80000000, polarity=0, phase=0, sck=sck, mosi=mosi, miso=miso) cs = Pin(5, Pin.OUT) dc = Pin(17, Pin.OUT) rst = Pin(27, Pin.OUT) busy = Pin(35, Pin.IN) self.epd = EPD(spi, cs, dc, rst, busy) self.epd.init() self.clear_screen() # we were in slow mode for the initial clear on startup, we're now going to fast mode as that's the most likely one we'll need next self.mode = 'slow' self.set_fast() self.running = False self.last_change = None def write(self, byteslike): self.textbuffer.write(byteslike) self.debounce_update() def set_slow(self): if self.mode == 'slow': return self.mode = 'slow' self.epd.set_slow() def set_fast(self): if self.mode == 'fast': return self.mode = 'fast' self.epd.set_fast() def plot(self, x, y): # rotate 90 degrees on the fly self.fb.pixel(font_height - 1 - y, x, 0) def plot_inverse(self, x, y): # rotate 90 degrees self.fb.pixel(font_height - 1 - y, x, 1) def debounce_update(self): self.last_change = time.ticks_ms() def update_screen(self, tmr=None): self.last_change = None if not self.running: return # TODO: keep some performance stats somewhere # the changed lines. keys are row indexes, values are line strings lines_dict = self.textbuffer.pop() # slow update if the entire screen changed (gives it a chance to remove the ghosting), otherwise fast for partial updates if len(lines_dict) == self.textbuffer.rows: self.set_slow() else: self.set_fast() cursor_x = self.textbuffer.x() cursor_y = self.textbuffer.y() # keep both buffers in the display's controller in sync for i in range(2): self._update_buffer(lines_dict, cursor_x, cursor_y) # display only one of the buffers if i == 0: self.epd.display_frame() def _update_buffer(self, lines_dict, cursor_x, cursor_y): for row_index, line in lines_dict.items(): # clear the framebuffer because it now represents this row self.fb.fill(1) # print the text font.draw_line(line.encode('ascii'), self.plot) if row_index == cursor_y: # draw the cursor (rotated 90 degrees) self.fb.fill_rect(0, cursor_x * font_width, font_height, font_width, 0) # if the cursor is on a character, also draw that character inverted if cursor_x < len(line): font.draw_line(line[cursor_x].encode('ascii'), self.plot_inverse, cursor_x * font_width, 1) # copy this row to the screen's buffer (rotated 90 degrees) self.epd.set_frame_memory( self.buf, screen_height - ((row_index + 1) * font_height), 0, font_height, screen_width) def clear_screen(self): self.fb.fill(1) # clear both buffers for i in range(2): for row_index in range(self.textbuffer.rows): self.epd.set_frame_memory(self.buf, row_index * font_height, 0, font_height, screen_width) # but only clear the screen once if i == 0: self.epd.display_frame() def clear(self): self.textbuffer.clear() self.debounce_update()
i2c = machine.I2C(scl=scl, sda=sda) frame_size = [64, 32] text = "Test" # Up to 8 characters in a 64px wide screen text_hsize = len(text) * 8 centered_text_start = [int((frame_size[0] / 2) - (text_hsize / 2)), int((frame_size[1] / 2) - (8 / 2))] # Frame buffers main_frame = FrameBuffer(bytearray(frame_size[0] * frame_size[1] // 8), frame_size[0], frame_size[1], MONO_HLSB) text_frame = FrameBuffer(bytearray(text_hsize * 8 // 8), text_hsize, 8, MONO_HLSB) pixel_frame = FrameBuffer(bytearray(1), 1, 1, MONO_HLSB) pixel_frame_black = FrameBuffer(bytearray(1), 1, 1, MONO_HLSB) # Text text_frame.fill(0) text_frame.text(text, 0, 0, 1) # Single-Pixels pixel_frame.fill(0) pixel_frame.pixel(0, 0, 1) pixel_frame_black.pixel(0, 0, 0) # Drawing text_frame on top of main_frame main_frame.fill(0) main_frame.blit(text_frame, centered_text_start[0], centered_text_start[1]) # Animation: underline for i in range(text_hsize): main_frame.blit(pixel_frame, centered_text_start[0] + i, centered_text_start[1] + 9)
class Display(object): """Serial interface for 2.9 inch E-paper display. Note: All coordinates are zero based. """ # Command constants from display datasheet CONTRAST_CONTROL = const(0x81) ENTIRE_DISPLAY_ON = const(0xA4) ALL_PIXELS_ON = const(0XA5) INVERSION_OFF = const(0xA6) INVERSION_ON = const(0XA7) DISPLAY_OFF = const(0xAE) DISPLAY_ON = const(0XAF) NOP = const(0xE3) COMMAND_LOCK = const(0xFD) CHARGE_PUMP = const(0x8D) # Scrolling commands CH_SCROLL_SETUP_RIGHT = const(0x26) CH_SCROLL_SETUP_LEFT = const(0x27) CV_SCROLL_SETUP_RIGHT = const(0x29) CV_SCROLL_SETUP_LEFT = const(0x2A) DEACTIVATE_SCROLL = const(0x2E) ACTIVATE_SCROLL = const(0x2F) VSCROLL_AREA = const(0xA3) SCROLL_SETUP_LEFT = const(0x2C) SCROLL_SETUP_RIGHT = const(0x2D) # Addressing commands LOW_CSA_IN_PAM = const(0x00) HIGH_CSA_IN_PAM = const(0x10) MEMORY_ADDRESSING_MODE = const(0x20) COLUMN_ADDRESS = const(0x21) PAGE_ADDRESS = const(0x22) PSA_IN_PAM = const(0xB0) DISPLAY_START_LINE = const(0x40) SEGMENT_MAP_REMAP = const(0xA0) SEGMENT_MAP_FLIPPED = const(0xA1) MUX_RATIO = const(0xA8) COM_OUTPUT_NORMAL = const(0xC0) COM_OUTPUT_FLIPPED = const(0xC8) DISPLAY_OFFSET = const(0xD3) COM_PINS_HW_CFG = const(0xDA) GPIO = const(0xDC) # Timing and driving scheme commands DISPLAY_CLOCK_DIV = const(0xd5) PRECHARGE_PERIOD = const(0xd9) VCOM_DESELECT_LEVEL = const(0xdb) def __init__(self, spi, cs, dc, rst, width=128, height=64): """Constructor for Display. Args: spi (Class Spi): SPI interface for display cs (Class Pin): Chip select pin dc (Class Pin): Data/Command pin rst (Class Pin): Reset pin width (Optional int): Screen width (default 128) height (Optional int): Screen height (default 64) """ self.spi = spi self.cs = cs self.dc = dc self.rst = rst self.width = width self.height = height self.pages = self.height // 8 self.byte_width = -(-width // 8) # Ceiling division self.buffer_length = self.byte_width * height # Buffer self.mono_image = bytearray(self.buffer_length) # Frame Buffer self.monoFB = FrameBuffer(self.mono_image, width, height, MONO_VLSB) self.clear_buffers() # Initialize GPIO pins 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() # Send initialization commands for cmd in ( self.DISPLAY_OFF, self.DISPLAY_CLOCK_DIV, 0x80, self.MUX_RATIO, self.height - 1, self.DISPLAY_OFFSET, 0x00, self.DISPLAY_START_LINE, self.CHARGE_PUMP, 0x14, self.MEMORY_ADDRESSING_MODE, 0x00, self.SEGMENT_MAP_FLIPPED, self.COM_OUTPUT_FLIPPED, self.COM_PINS_HW_CFG, 0x02 if (self.height == 32 or self.height == 16) and (self.width != 64) else 0x12, self.CONTRAST_CONTROL, 0xFF, self.PRECHARGE_PERIOD, 0xF1, self.VCOM_DESELECT_LEVEL, 0x40, self.ENTIRE_DISPLAY_ON, # output follows RAM contents self.INVERSION_OFF, # not inverted self.DISPLAY_ON): # on self.write_cmd(cmd) self.clear_buffers() self.present() def cleanup(self): """Clean up resources.""" self.clear() self.sleep() self.spi.deinit() print('display off') def clear(self): """Clear display. """ self.clear_buffers() self.present() def clear_buffers(self): """Clear buffer. """ self.monoFB.fill(0x00) def draw_bitmap(self, path, x, y, w, h, invert=False, rotate=0): """Load MONO_HMSB bitmap from disc and draw to screen. Args: path (string): Image file path. x (int): x-coord of image. y (int): y-coord of image. w (int): Width of image. h (int): Height of image. invert (bool): True = invert image, False (Default) = normal image. rotate(int): 0, 90, 180, 270 Notes: w x h cannot exceed 2048 """ array_size = w * h with open(path, "rb") as f: buf = bytearray(f.read(array_size)) fb = FrameBuffer(buf, w, h, MONO_HMSB) if rotate == 0 and invert is True: # 0 degrees fb2 = FrameBuffer(bytearray(array_size), w, h, MONO_HMSB) for y1 in range(h): for x1 in range(w): fb2.pixel(x1, y1, fb.pixel(x1, y1) ^ 0x01) fb = fb2 elif rotate == 90: # 90 degrees byte_width = (w - 1) // 8 + 1 adj_size = h * byte_width fb2 = FrameBuffer(bytearray(adj_size), h, w, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel(y1, x1, fb.pixel(x1, (h - 1) - y1) ^ 0x01) else: fb2.pixel(y1, x1, fb.pixel(x1, (h - 1) - y1)) fb = fb2 elif rotate == 180: # 180 degrees fb2 = FrameBuffer(bytearray(array_size), w, h, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel( x1, y1, fb.pixel((w - 1) - x1, (h - 1) - y1) ^ 0x01) else: fb2.pixel(x1, y1, fb.pixel((w - 1) - x1, (h - 1) - y1)) fb = fb2 elif rotate == 270: # 270 degrees byte_width = (w - 1) // 8 + 1 adj_size = h * byte_width fb2 = FrameBuffer(bytearray(adj_size), h, w, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel(y1, x1, fb.pixel((w - 1) - x1, y1) ^ 0x01) else: fb2.pixel(y1, x1, fb.pixel((w - 1) - x1, y1)) fb = fb2 self.monoFB.blit(fb, x, y) def draw_bitmap_raw(self, path, x, y, w, h, invert=False, rotate=0): """Load raw bitmap from disc and draw to screen. Args: path (string): Image file path. x (int): x-coord of image. y (int): y-coord of image. w (int): Width of image. h (int): Height of image. invert (bool): True = invert image, False (Default) = normal image. rotate(int): 0, 90, 180, 270 Notes: w x h cannot exceed 2048 """ if rotate == 90 or rotate == 270: w, h = h, w # Swap width & height if landscape buf_size = w * h with open(path, "rb") as f: if rotate == 0: buf = bytearray(f.read(buf_size)) elif rotate == 90: buf = bytearray(buf_size) for x1 in range(w - 1, -1, -1): for y1 in range(h): index = (w * y1) + x1 buf[index] = f.read(1)[0] elif rotate == 180: buf = bytearray(buf_size) for index in range(buf_size - 1, -1, -1): buf[index] = f.read(1)[0] elif rotate == 270: buf = bytearray(buf_size) for x1 in range(1, w + 1): for y1 in range(h - 1, -1, -1): index = (w * y1) + x1 - 1 buf[index] = f.read(1)[0] if invert: for i, _ in enumerate(buf): buf[i] ^= 0xFF fbuf = FrameBuffer(buf, w, h, GS8) self.monoFB.blit(fbuf, x, y) def draw_circle(self, x0, y0, r, invert=False): """Draw a circle. Args: x0 (int): X coordinate of center point. y0 (int): Y coordinate of center point. r (int): Radius. invert (bool): True = clear line, False (Default) = draw line. """ f = 1 - r dx = 1 dy = -r - r x = 0 y = r self.draw_pixel(x0, y0 + r, invert) self.draw_pixel(x0, y0 - r, invert) self.draw_pixel(x0 + r, y0, invert) self.draw_pixel(x0 - r, y0, invert) 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, invert) self.draw_pixel(x0 - x, y0 + y, invert) self.draw_pixel(x0 + x, y0 - y, invert) self.draw_pixel(x0 - x, y0 - y, invert) self.draw_pixel(x0 + y, y0 + x, invert) self.draw_pixel(x0 - y, y0 + x, invert) self.draw_pixel(x0 + y, y0 - x, invert) self.draw_pixel(x0 - y, y0 - x, invert) def draw_ellipse(self, x0, y0, a, b, invert=False): """Draw an ellipse. Args: x0, y0 (int): Coordinates of center point. a (int): Semi axis horizontal. b (int): Semi axis vertical. invert (bool): True = clear line, False (Default) = draw line. 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, invert) self.draw_pixel(x0 - x, y0 + y, invert) self.draw_pixel(x0 + x, y0 - y, invert) self.draw_pixel(x0 - x, y0 - y, invert) # 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, invert) self.draw_pixel(x0 - x, y0 + y, invert) self.draw_pixel(x0 + x, y0 - y, invert) self.draw_pixel(x0 - x, y0 - y, invert) # 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, invert) self.draw_pixel(x0 - x, y0 + y, invert) self.draw_pixel(x0 + x, y0 - y, invert) self.draw_pixel(x0 - x, y0 - y, invert) def draw_hline(self, x, y, w, invert=False): """Draw a horizontal line. Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of line. invert (bool): True = clear line, False (Default) = draw line. """ if self.is_off_grid(x, y, x + w - 1, y): return self.monoFB.hline(x, y, w, int(invert ^ 1)) def draw_letter(self, x, y, letter, font, invert=False, rotate=False): """Draw a letter. Args: x (int): Starting X position. y (int): Starting Y position. letter (string): Letter to draw. font (XglcdFont object): Font. invert (bool): Invert color rotate (int): Rotation of letter """ fbuf, w, h = font.get_letter(letter, invert=invert, rotate=rotate) # Check for errors if w == 0: return w, h # Offset y for 270 degrees and x for 180 degrees if rotate == 180: x -= w elif rotate == 270: y -= h self.monoFB.blit(fbuf, x, y) return w, h def draw_line(self, x1, y1, x2, y2, invert=False): """Draw a line using Bresenham's algorithm. Args: x1, y1 (int): Starting coordinates of the line x2, y2 (int): Ending coordinates of the line invert (bool): True = clear line, False (Default) = draw line. """ # Check for horizontal line if y1 == y2: if x1 > x2: x1, x2 = x2, x1 self.draw_hline(x1, y1, x2 - x1 + 1, invert) return # Check for vertical line if x1 == x2: if y1 > y2: y1, y2 = y2, y1 self.draw_vline(x1, y1, y2 - y1 + 1, invert) return # Confirm coordinates in boundary if self.is_off_grid(min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)): return self.monoFB.line(x1, y1, x2, y2, invert ^ 1) def draw_lines(self, coords, invert=False): """Draw multiple lines. Args: coords ([[int, int],...]): Line coordinate X, Y pairs invert (bool): True = clear line, False (Default) = draw line. """ # 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, invert) x1, y1 = x2, y2 def draw_pixel(self, x, y, invert=False): """Draw a single pixel. Args: x (int): X position. y (int): Y position. invert (bool): True = clear line, False (Default) = draw line. """ if self.is_off_grid(x, y, x, y): return self.monoFB.pixel(x, y, int(invert ^ 1)) def draw_polygon(self, sides, x0, y0, r, invert=False, 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. invert (bool): True = clear line, False (Default) = draw line. 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, invert) def draw_rectangle(self, x, y, w, h, invert=False): """Draw a rectangle. Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of rectangle. h (int): Height of rectangle. invert (bool): True = clear line, False (Default) = draw line. """ self.monoFB.rect(x, y, w, h, int(invert ^ 1)) def draw_sprite(self, fbuf, x, y, w, h): """Draw a sprite. Args: fbuf (FrameBuffer): 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.monoFB.blit(fbuf, x, y) def draw_text(self, x, y, text, font, invert=False, rotate=0, spacing=1): """Draw text. Args: x (int): Starting X position. y (int): Starting Y position. text (string): Text to draw. font (XglcdFont object): Font. invert (bool): Invert color rotate (int): Rotation of letter spacing (int): Pixels between letters (default: 1) """ for letter in text: # Get letter array and letter dimensions w, h = self.draw_letter(x, y, letter, font, invert, rotate) # Stop on error if w == 0 or h == 0: return if rotate == 0: # Fill in spacing if spacing: self.fill_rectangle(x + w, y, spacing, h, invert ^ 1) # Position x for next letter x += (w + spacing) elif rotate == 90: # Fill in spacing if spacing: self.fill_rectangle(x, y + h, w, spacing, invert ^ 1) # Position y for next letter y += (h + spacing) elif rotate == 180: # Fill in spacing if spacing: self.fill_rectangle(x - w - spacing, y, spacing, h, invert ^ 1) # Position x for next letter x -= (w + spacing) elif rotate == 270: # Fill in spacing if spacing: self.fill_rectangle(x, y - h - spacing, w, spacing, invert ^ 1) # Position y for next letter y -= (h + spacing) else: print("Invalid rotation.") return def draw_text8x8(self, x, y, text): """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. """ # Confirm coordinates in boundary if self.is_off_grid(x, y, x + 8, y + 8): return self.monoFB.text(text, x, y) def draw_vline(self, x, y, h, invert=False): """Draw a vertical line. Args: x (int): Starting X position. y (int): Starting Y position. h (int): Height of line. invert (bool): True = clear line, False (Default) = draw line. """ # Confirm coordinates in boundary if self.is_off_grid(x, y, x, y + h): return self.monoFB.vline(x, y, h, int(invert ^ 1)) def fill_circle(self, x0, y0, r, invert=False): """Draw a filled circle. Args: x0 (int): X coordinate of center point. y0 (int): Y coordinate of center point. r (int): Radius. invert (bool): True = clear line, False (Default) = draw line. """ f = 1 - r dx = 1 dy = -r - r x = 0 y = r self.draw_vline(x0, y0 - r, 2 * r + 1, invert) 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, invert) self.draw_vline(x0 - x, y0 - y, 2 * y + 1, invert) self.draw_vline(x0 - y, y0 - x, 2 * x + 1, invert) self.draw_vline(x0 + y, y0 - x, 2 * x + 1, invert) def fill_ellipse(self, x0, y0, a, b, invert=False): """Draw a filled ellipse. Args: x0, y0 (int): Coordinates of center point. a (int): Semi axis horizontal. b (int): Semi axis vertical. invert (bool): True = clear line, False (Default) = draw line. 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, invert) # 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, invert) self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, invert) # 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, invert) self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, invert) def fill_rectangle(self, x, y, w, h, invert=False): """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. visble (bool): True (Default) = draw line, False = clear line. """ if self.is_off_grid(x, y, x + w - 1, y + h - 1): return self.monoFB.fill_rect(x, y, w, h, int(invert ^ 1)) def fill_polygon(self, sides, x0, y0, r, invert=False, 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. visble (bool): True (Default) = draw line, False = clear line. 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, invert) 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, invert=False, rotate=0): """Load MONO_HMSB bitmap from disc to sprite. Args: path (string): Image file path. w (int): Width of image. h (int): Height of image. invert (bool): True = invert image, False (Default) = normal image. rotate(int): 0, 90, 180, 270 Notes: w x h cannot exceed 2048 """ array_size = w * h with open(path, "rb") as f: buf = bytearray(f.read(array_size)) fb = FrameBuffer(buf, w, h, MONO_HMSB) if rotate == 0 and invert is True: # 0 degrees fb2 = FrameBuffer(bytearray(array_size), w, h, MONO_HMSB) for y1 in range(h): for x1 in range(w): fb2.pixel(x1, y1, fb.pixel(x1, y1) ^ 0x01) fb = fb2 elif rotate == 90: # 90 degrees byte_width = (w - 1) // 8 + 1 adj_size = h * byte_width fb2 = FrameBuffer(bytearray(adj_size), h, w, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel(y1, x1, fb.pixel(x1, (h - 1) - y1) ^ 0x01) else: fb2.pixel(y1, x1, fb.pixel(x1, (h - 1) - y1)) fb = fb2 elif rotate == 180: # 180 degrees fb2 = FrameBuffer(bytearray(array_size), w, h, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel( x1, y1, fb.pixel((w - 1) - x1, (h - 1) - y1) ^ 0x01) else: fb2.pixel(x1, y1, fb.pixel((w - 1) - x1, (h - 1) - y1)) fb = fb2 elif rotate == 270: # 270 degrees byte_width = (w - 1) // 8 + 1 adj_size = h * byte_width fb2 = FrameBuffer(bytearray(adj_size), h, w, MONO_HMSB) for y1 in range(h): for x1 in range(w): if invert is True: fb2.pixel(y1, x1, fb.pixel((w - 1) - x1, y1) ^ 0x01) else: fb2.pixel(y1, x1, fb.pixel((w - 1) - x1, y1)) fb = fb2 return fb def present(self): """Present image to display. """ x0 = 0 x1 = self.width - 1 if self.width == 64: # displays with width of 64 pixels are shifted by 32 x0 += 32 x1 += 32 self.write_cmd(self.COLUMN_ADDRESS) self.write_cmd(x0) self.write_cmd(x1) self.write_cmd(self.PAGE_ADDRESS) self.write_cmd(0) self.write_cmd(self.pages - 1) self.write_data(self.mono_image) def reset(self): """Perform reset.""" self.rst(1) sleep_ms(1) self.rst(0) sleep_ms(10) self.rst(1) def sleep(self): """Put display to sleep.""" self.write_cmd(self.DISPLAY_OFF) def wake(self): """Wake display from sleep.""" self.write_cmd(self.DISPLAY_ON) def write_cmd(self, command, *args): """Write command to display. Args: command (byte): Display 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_data(self, data): """Write data to display. Args: data (bytes): Data to transmit. """ self.dc(1) self.cs(0) self.spi.write(data) self.cs(1)
class SSD1306_I2C: def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False): self.i2c = i2c self.addr = addr self.temp = bytearray(2) self.width = width self.height = height self.external_vcc = external_vcc self.pages = self.height // 8 self.buffer = bytearray(self.pages * self.width) from framebuf import FrameBuffer, MVLSB self.framebuf = FrameBuffer(self.buffer, self.width, self.height, MVLSB) self.init_display() def write_cmd(self, cmd): self.temp[0] = 0x80 # Co=1, D/C#=0 self.temp[1] = cmd self.i2c.writeto(self.addr, self.temp) def write_data(self, buf): self.temp[0] = self.addr << 1 self.temp[1] = 0x40 # Co=0, D/C#=1 self.i2c.start() self.i2c.write(self.temp) self.i2c.write(buf) self.i2c.stop() def init_display(self): for cmd in (0xae | 0x00, 0x20, 0x00, 0x40 | 0x00, 0xa0 | 0x01, 0xa8, self.height - 1, 0xc0 | 0x08, 0xd3, 0x00, 0xda, 0x02 if self.height == 32 else 0x12, 0xd5, 0x80, 0xd9, 0x22 if self.external_vcc else 0xf1, 0xdb, 0x30, 0x81, 0xff, 0xa4, 0xa6, 0x8d, 0x10 if self.external_vcc else 0x14, 0xae | 0x01): self.write_cmd(cmd) self.fill(0) self.show() def poweroff(self): self.write_cmd(0xae | 0x00) def contrast(self, contrast): self.write_cmd(0x81) self.write_cmd(contrast) def invert(self, invert): self.write_cmd(0xa6 | (invert & 1)) def show(self): x0 = 0 x1 = self.width - 1 if self.width == 64: x0 += 32 x1 += 32 self.write_cmd(0x21) self.write_cmd(x0) self.write_cmd(x1) self.write_cmd(0x22) self.write_cmd(0) self.write_cmd(self.pages - 1) self.write_data(self.buffer) def fill(self, col): self.framebuf.fill(col) def pixel(self, x, y, col): self.framebuf.pixel(x, y, col) def scroll(self, dx, dy): self.framebuf.scroll(dx, dy) def text(self, string, x, y, col=1): self.framebuf.text(string, x, y, col)