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()
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)