Exemplo n.º 1
0
 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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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)