From c6ce9c6654268bff751e427e6bc6e077105bad4a Mon Sep 17 00:00:00 2001 From: Dejvino Date: Wed, 19 Apr 2023 20:04:02 +0200 Subject: [PATCH] Initial version - cycling of BMP images --- epaper.py | 309 +++++++++++++++++++++++++++++++ gallery/convert_image.sh | 11 ++ microbmp.py | 382 +++++++++++++++++++++++++++++++++++++++ test.py | 68 +++++++ 4 files changed, 770 insertions(+) create mode 100644 epaper.py create mode 100755 gallery/convert_image.sh create mode 100644 microbmp.py create mode 100644 test.py diff --git a/epaper.py b/epaper.py new file mode 100644 index 0000000..8fde4e4 --- /dev/null +++ b/epaper.py @@ -0,0 +1,309 @@ +# ***************************************************************************** +# * | File : Pico_ePaper-5.65.py +# * | Author : Waveshare team +# * | Function : Electronic paper driver +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2021-06-04 +# # | Info : python demo +# ----------------------------------------------------------------------------- +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documnetation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ----------------- +# Source: https://github.com/waveshare/Pico_ePaper_Code/blob/main/python/Pico-ePaper-5.65f.py +# Wiki: https://www.waveshare.com/wiki/Pico-ePaper-5.65 +# +from machine import Pin, SPI +import framebuf +import utime + +# Display resolution +EPD_WIDTH = 600 +EPD_HEIGHT = 448 + +RST_PIN = 12 +DC_PIN = 8 +CS_PIN = 9 +BUSY_PIN = 13 + +class EPD_5in65(framebuf.FrameBuffer): + def __init__(self): + self.reset_pin = Pin(RST_PIN, Pin.OUT) + + self.busy_pin = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP) + self.cs_pin = Pin(CS_PIN, Pin.OUT) + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + + self.Black = 0x00 + self.White = 0x01 + self.Green = 0x02 + self.Blue = 0x03 + self.Red = 0x04 + self.Yellow = 0x05 + self.Orange = 0x06 + self.Clean = 0x07 + + + self.spi = SPI(1) + self.spi.init(baudrate=4000_000) + self.dc_pin = Pin(DC_PIN, Pin.OUT) + + + self.buffer = bytearray(self.height * self.width // 2) + super().__init__(self.buffer, self.width, self.height, framebuf.GS4_HMSB) + + self.EPD_5IN65F_Init() + + def digital_write(self, pin, value): + pin.value(value) + + def digital_read(self, pin): + return pin.value() + + def delay_ms(self, delaytime): + utime.sleep(delaytime / 1000.0) + + def spi_writebyte(self, data): + self.spi.write(bytearray(data)) + + def module_exit(self): + self.digital_write(self.reset_pin, 0) + print("ePaper exited") + + # Hardware reset + def reset(self): + self.digital_write(self.reset_pin, 1) + self.delay_ms(200) + self.digital_write(self.reset_pin, 0) + self.delay_ms(1) + self.digital_write(self.reset_pin, 1) + self.delay_ms(200) + print("ePaper reset") + + def send_command(self, command): + self.digital_write(self.dc_pin, 0) + self.digital_write(self.cs_pin, 0) + self.spi_writebyte([command]) + self.digital_write(self.cs_pin, 1) + + def send_data(self, data): + self.digital_write(self.dc_pin, 1) + self.digital_write(self.cs_pin, 0) + self.spi_writebyte([data]) + self.digital_write(self.cs_pin, 1) + + def send_data1(self, buf): + self.digital_write(self.dc_pin, 1) + self.digital_write(self.cs_pin, 0) + self.spi.write(bytearray(buf)) + self.digital_write(self.cs_pin, 1) + + def BusyHigh(self): + while(self.digital_read(self.busy_pin) == 0): + self.delay_ms(1) + + def BusyLow(self): + while(self.digital_read(self.busy_pin) == 1): + self.delay_ms(1) + + def EPD_5IN65F_Init(self): + + self.reset(); + self.BusyHigh(); + self.send_command(0x00); + self.send_data(0xEF); + self.send_data(0x08); + self.send_command(0x01); + self.send_data(0x37); + self.send_data(0x00); + self.send_data(0x23); + self.send_data(0x23); + self.send_command(0x03); + self.send_data(0x00); + self.send_command(0x06); + self.send_data(0xC7); + self.send_data(0xC7); + self.send_data(0x1D); + self.send_command(0x30); + self.send_data(0x3C); + self.send_command(0x41); + self.send_data(0x00); + self.send_command(0x50); + self.send_data(0x37); + self.send_command(0x60); + self.send_data(0x22); + self.send_command(0x61); + self.send_data(0x02); + self.send_data(0x58); + self.send_data(0x01); + self.send_data(0xC0); + self.send_command(0xE3); + self.send_data(0xAA); + + self.delay_ms(100); + self.send_command(0x50); + self.send_data(0x37); + print("ePaper inited") + + def EPD_5IN65F_Clear(self,color): + + self.send_command(0x61) # Set Resolution setting + self.send_data(0x02) + self.send_data(0x58) + self.send_data(0x01) + self.send_data(0xC0) + self.send_command(0x10) + for i in range(0,int(self.width / 2)): + self.send_data1([(color<<4)|color] * self.height) + + self.send_command(0x04) # 0x04 + self.BusyHigh() + self.send_command(0x12) # 0x12 + self.BusyHigh() + self.send_command(0x02) # 0x02 + self.BusyLow() + self.delay_ms(500) + print("ePaper cleared") + + def EPD_5IN65F_Display(self,image): + + self.send_command(0x61) # Set Resolution setting + self.send_data(0x02) + self.send_data(0x58) + self.send_data(0x01) + self.send_data(0xC0) + self.send_command(0x10) + + for i in range(0, int(self.width // 2)): + self.send_data1(image[(i*self.height):((i+1)*self.height)]) + + self.send_command(0x04) # 0x04 + self.BusyHigh() + self.send_command(0x12) # 0x12 + self.BusyHigh() + self.send_command(0x02) # 0x02 + self.BusyLow() + self.delay_ms(200) + print("ePaper displayed image") + + def EPD_5IN65F_Display_part(self,image,xstart,ystart,image_width,image_heigh): + + self.send_command(0x61) # Set Resolution setting + self.send_data(0x02) + self.send_data(0x58) + self.send_data(0x01) + self.send_data(0xC0) + self.send_command(0x10) + for i in range(0, self.height): + for j in range(0, int(self.width / 2)): + if((i<(image_heigh+ystart)) & (i>(ystart-1) ) & (j<(image_width+xstart)/2) & (j>(xstart/2 - 1))): + self.send_data(image[(j-xstart/2) + (image_width/2*(i-ystart))]) + else: + self.send_data(0x11) + + self.send_command(0x04) # 0x04 + self.BusyHigh() + self.send_command(0x12) # 0x12 + self.BusyHigh() + self.send_command(0x02) # 0x02 + self.BusyLow() + self.delay_ms(200) + print("ePaper displayed part") + + + + def Sleep(self): + self.delay_ms(100); + self.send_command(0x07); + self.send_data(0xA5); + self.delay_ms(100); + self.digital_write(self.reset_pin, 1) + print("ePaper entered sleep") + + +if __name__=='__main__': + + print("ePaper test starting") + + epd = EPD_5in65() + + epd.EPD_5IN65F_Clear(epd.White) + + epd.fill(0xff) + + epd.text("Waveshare", 5, 5, epd.Black) + epd.text("Pico_ePaper-5.65", 5, 20, epd.Black) + epd.text("Raspberry Pico", 5, 35, epd.Black) + + epd.EPD_5IN65F_Display(epd.buffer) + print("Test: text") + epd.delay_ms(5000) + + epd.vline(10, 60, 60, epd.Black) + epd.vline(90, 60, 60, epd.Black) + epd.hline(10, 60, 80, epd.Black) + epd.hline(10, 120, 80, epd.Black) + epd.line(10, 60, 90, 120, epd.Black) + epd.line(90, 60, 10, 120, epd.Black) + + epd.rect(10, 136, 50, 80, epd.Black) + epd.fill_rect(70, 136, 50, 80, epd.Black) + + epd.EPD_5IN65F_Display(epd.buffer) + print("Test: black lines and rects") + epd.delay_ms(5000) + + epd.text('Black',200,11,epd.Black) + epd.fill_rect(300, 0, 300, 30, epd.Black) + epd.text('White',200,41,epd.White) + epd.fill_rect(300, 30, 300, 30, epd.White) + epd.text('Green',200,71,epd.Green) + epd.fill_rect(300, 60, 300, 30, epd.Green) + epd.text('Blue',200,101,epd.Blue) + epd.fill_rect(300, 90, 300, 30, epd.Blue) + epd.text('Red',200,131,epd.Red) + epd.fill_rect(300, 120, 300, 30, epd.Red) + epd.text('Yellow',200,161,epd.Yellow) + epd.fill_rect(300, 150, 300, 30, epd.Yellow) + epd.text('Orange',200,191,epd.Orange) + epd.fill_rect(300, 180, 300, 30, epd.Orange) + epd.text('Clean',200,221,epd.Black) + epd.fill_rect(300, 210, 300, 30, epd.Clean) + epd.EPD_5IN65F_Display(epd.buffer) + print("Test: color names") + epd.delay_ms(5000) + + j = 0 + for i in range(-250,600): + epd.line(i, 238, i+250, 448, j) + if (i%30==0) : + j = j+1 + j = j%7 + epd.EPD_5IN65F_Display(epd.buffer) + print("Test: color lines") + epd.delay_ms(5000) + + epd.EPD_5IN65F_Clear(epd.White) + + epd.Sleep() + + print("END") + diff --git a/gallery/convert_image.sh b/gallery/convert_image.sh new file mode 100755 index 0000000..4074d08 --- /dev/null +++ b/gallery/convert_image.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# additional options: +# -posterize 3 + +SRC=$1 +DST=$2 +shift 2 + +convert "$SRC" $@ -resize 600x448 -dither FloydSteinberg -type Palette -remap palette.bmp "$DST" + diff --git a/microbmp.py b/microbmp.py new file mode 100644 index 0000000..8a623e0 --- /dev/null +++ b/microbmp.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +"""A small Python module for BMP image processing. + +- Author: Quan Lin +- Adapted by: Dejvino +- Adapted from: https://github.com/jacklinquan/micropython-microbmp +- License: MIT +""" + +from struct import pack, unpack + +# Project Version +__version__ = "0.1.0" +__all__ = ["MicroBMP"] + + +class MicroBMP(object): + def __init__(self, width=None, height=None, depth=None, palette=None, data_callback=None): + # BMP Header + self.BMP_id = b"BM" + self.BMP_size = None + self.BMP_reserved1 = b"\x00\x00" + self.BMP_reserved2 = b"\x00\x00" + self.BMP_offset = None + + # DIB Header + self.DIB_len = 40 + self.DIB_w = width + self.DIB_h = height + self.DIB_planes_num = 1 + self.DIB_depth = depth + self.DIB_comp = 0 + self.DIB_raw_size = None + self.DIB_hres = 2835 # 72 DPI * 39.3701 inches/metre. + self.DIB_vres = 2835 + self.DIB_num_in_plt = None + self.DIB_extra = None + + self.palette = palette + self.parray = None # Pixel array + + self.ppb = None # Number of pixels per byte for depth <= 8. + self.pmask = None # Pixel Mask + self.row_size = None + self.padded_row_size = None + + self.data_callback = data_callback + + self.initialised = False + self._init() + + def __getitem__(self, key): + assert self.initialised, "Image not initialised!" + assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!" + + # Pixels are arranged in HLSB format with high bits being the leftmost + pindex = key[1] * self.DIB_w + key[0] # Pixel index + if self.DIB_depth <= 8: + return self._extract_from_bytes(self.parray, pindex) + else: + pindex *= 3 + if (len(key) > 2) and (key[2] in (0, 1, 2)): + return self.parray[pindex + key[2]] + else: + return ( + self.parray[pindex], + self.parray[pindex + 1], + self.parray[pindex + 2], + ) + + def __setitem__(self, key, value): + assert self.initialised, "Image not initialised!" + assert key[0] < self.DIB_w and key[1] < self.DIB_h, "Out of image boundary!" + + # Pixels are arranged in HLSB format with high bits being the leftmost + pindex = key[1] * self.DIB_w + key[0] # Pixel index + if self.DIB_depth <= 8: + self._fill_in_bytes(self.parray, pindex, value) + else: + pindex *= 3 + if (len(key) > 2) and (key[2] in (0, 1, 2)): + self.parray[pindex + key[2]] = value + else: + self.parray[pindex] = value[0] + self.parray[pindex + 1] = value[1] + self.parray[pindex + 2] = value[2] + + def __str__(self): + if not self.initialised: + return repr(self) + + return "BMP image, {}, {}-bit, {}x{} pixels, {} bytes".format( + "indexed" if self.DIB_depth <= 8 else "RGB", + self.DIB_depth, + self.DIB_w, + self.DIB_h, + self.BMP_size, + ) + + def _init(self): + if None in (self.DIB_w, self.DIB_h, self.DIB_depth): + self.initialised = False + return self.initialised + + assert self.BMP_id == b"BM", "BMP id ({}) must be b'BM'!".format(self.BMP_id) + assert ( + len(self.BMP_reserved1) == 2 and len(self.BMP_reserved2) == 2 + ), "Length of BMP reserved fields ({}+{}) must be 2+2!".format( + len(self.BMP_reserved1), len(self.BMP_reserved2) + ) + assert self.DIB_planes_num == 1, "DIB planes number ({}) must be 1!".format( + self.DIB_planes_num + ) + assert self.DIB_depth in ( + 1, + 2, + 4, + 8, + 24, + ), "Colour depth ({}) must be in (1, 2, 4, 8, 24)!".format(self.DIB_depth) + assert ( + self.DIB_comp == 0 + or (self.DIB_depth == 8 and self.DIB_comp == 1) + or (self.DIB_depth == 4 and self.DIB_comp == 2) + ), "Colour depth + compression ({}+{}) must be X+0/8+1/4+2!".format( + self.DIB_depth, self.DIB_comp + ) + + if self.DIB_depth <= 8: + self.ppb = 8 // self.DIB_depth + self.pmask = 0xFF >> (8 - self.DIB_depth) + if self.palette is None: + # Default palette is black and white or full size grey scale. + self.DIB_num_in_plt = 2 ** self.DIB_depth + self.palette = [None for i in range(self.DIB_num_in_plt)] + for i in range(self.DIB_num_in_plt): + # Assignment that suits all: 1/2/4/8-bit colour depth. + s = 255 * i // (self.DIB_num_in_plt - 1) + self.palette[i] = bytearray([s, s, s]) + else: + self.DIB_num_in_plt = len(self.palette) + else: + self.ppb = None + self.pmask = None + self.DIB_num_in_plt = 0 + self.palette = None + + #if self.parray is None: + # if self.DIB_depth <= 8: + # div, mod = divmod(self.DIB_w * self.DIB_h, self.ppb) + # self.parray = bytearray(div + (1 if mod else 0)) + # else: + # self.parray = bytearray(self.DIB_w * self.DIB_h * 3) + + plt_size = self.DIB_num_in_plt * 4 + self.BMP_offset = 14 + self.DIB_len + plt_size + self.row_size = self._size_from_width(self.DIB_w) + self.padded_row_size = self._padded_size_from_size(self.row_size) + if self.DIB_comp == 0: + self.DIB_raw_size = self.padded_row_size * self.DIB_h + self.BMP_size = self.BMP_offset + self.DIB_raw_size + + self.initialised = True + return self.initialised + + def _size_from_width(self, width): + return (width * self.DIB_depth + 7) // 8 + + def _padded_size_from_size(self, size): + return (size + 3) // 4 * 4 + + def _extract_from_bytes(self, data, index): + # One formula that suits all: 1/2/4/8-bit colour depth. + byte_index, pos_in_byte = divmod(index, self.ppb) + shift = 8 - self.DIB_depth * (pos_in_byte + 1) + return (data[byte_index] >> shift) & self.pmask + + def _fill_in_bytes(self, data, index, value): + # One formula that suits all: 1/2/4/8-bit colour depth. + byte_index, pos_in_byte = divmod(index, self.ppb) + shift = 8 - self.DIB_depth * (pos_in_byte + 1) + value &= self.pmask + data[byte_index] = (data[byte_index] & ~(self.pmask << shift)) + ( + value << shift + ) + + def _decode_rle(self, bf_io): + # Only bottom-up bitmap can be compressed. + x, y = 0, self.DIB_h - 1 + while True: + data = bf_io.read(2) + if data[0] == 0: + if data[1] == 0: + x, y = 0, y - 1 + elif data[1] == 1: + return + elif data[1] == 2: + data = bf_io.read(2) + x, y = x + data[0], y - data[1] + else: + num_of_pixels = data[1] + num_to_read = (self._size_from_width(num_of_pixels) + 1) // 2 * 2 + data = bf_io.read(num_to_read) + for i in range(num_of_pixels): + self[x, y] = self._extract_from_bytes(data, i) + x += 1 + else: + b = bytes([data[1]]) + for i in range(data[0]): + self[x, y] = self._extract_from_bytes(b, i % self.ppb) + x += 1 + + def read_io(self, bf_io): + print("BMP reading file") + # BMP Header + data = bf_io.read(14) + self.BMP_id = data[0:2] + self.BMP_size = unpack(" 40: + self.DIB_extra = data[36:] + + # Palette + if self.DIB_depth <= 8: + if DIB_plt_num_info == 0: + self.DIB_num_in_plt = 2 ** self.DIB_depth + else: + self.DIB_num_in_plt = DIB_plt_num_info + self.palette = [None for i in range(self.DIB_num_in_plt)] + for i in range(self.DIB_num_in_plt): + data = bf_io.read(4) + colour = bytearray([data[2], data[1], data[0]]) + self.palette[i] = colour + + # In case self.DIB_h < 0 for top-down format. + if self.DIB_h < 0: + self.DIB_h = -self.DIB_h + is_top_down = True + else: + is_top_down = False + + self.parray = None + assert self._init(), "Failed to initialize the image!" + + # Pixels + print("BMP reading data") + if self.DIB_comp == 0: + # BI_RGB + for h in range(self.DIB_h): + y = h if is_top_down else self.DIB_h - h - 1 + data = bf_io.read(self.padded_row_size) + for x in range(self.DIB_w): + if self.DIB_depth <= 8: + #self[x, y] = self._extract_from_bytes(data, x) + pixel_data = self._extract_from_bytes(data, x) + pixel = pixel_data if self.palette is None else self.palette[pixel_data] + self.data_callback(x, y, pixel) + else: + v = x * 3 + # BMP colour is in BGR order. + self[x, y] = (data[v + 2], data[v + 1], data[v]) + else: + # BI_RLE8 or BI_RLE4 + self._decode_rle(bf_io) + + print("BMP done") + return self + + def write_io(self, bf_io, force_40B_DIB=False): + if force_40B_DIB: + self.DIB_len = 40 + self.DIB_extra = None + + # Only uncompressed image is supported to write. + self.DIB_comp = 0 + + assert self._init(), "Failed to initialize the image!" + + # BMP Header + bf_io.write(self.BMP_id) + bf_io.write(pack(" 40: + bf_io.write(self.DIB_extra) + + # Palette + if self.DIB_depth <= 8: + for colour in self.palette: + bf_io.write(bytes([colour[2], colour[1], colour[0], 0])) + + # Pixels + for h in range(self.DIB_h): + # BMP last row comes first. + y = self.DIB_h - h - 1 + if self.DIB_depth <= 8: + d = 0 + for x in range(self.DIB_w): + self[x, y] %= self.DIB_num_in_plt + # One formula that suits all: 1/2/4/8-bit colour depth. + d = (d << (self.DIB_depth % 8)) + self[x, y] + if x % self.ppb == self.ppb - 1: + # Got a whole byte. + bf_io.write(bytes([d])) + d = 0 + if x % self.ppb != self.ppb - 1: + # Last byte if width does not fit in whole bytes. + d <<= ( + 8 + - self.DIB_depth + - (x % self.ppb) * (2 ** (self.DIB_depth - 1)) + ) + bf_io.write(bytes([d])) + d = 0 + else: + for x in range(self.DIB_w): + r, g, b = self[x, y] + bf_io.write(bytes([b, g, r])) + # Pad row to multiple of 4 bytes with 0x00. + bf_io.write(b"\x00" * (self.padded_row_size - self.row_size)) + + num_of_bytes = bf_io.tell() + return num_of_bytes + + def load(self, file_path): + with open(file_path, "rb") as file: + self.read_io(file) + return self + + def save(self, file_path, force_40B_DIB=False): + with open(file_path, "wb") as file: + num_of_bytes = self.write_io(file, force_40B_DIB) + return num_of_bytes diff --git a/test.py b/test.py new file mode 100644 index 0000000..0a2c049 --- /dev/null +++ b/test.py @@ -0,0 +1,68 @@ +import epaper +import microbmp +import time +import gc + +colormap = [ + [0x00, 0x00, 0x00], # black + [0xff, 0xff, 0xff], # white + [0x00, 0xdd, 0x00], # green + [0x00, 0x00, 0xee], # blue + [0x00, 0xdd, 0x00], # red + [0xff, 0xdd, 0x00], # yellow + [0xff, 0x88, 0x00], # orange + ] + +images = ["lambo.bmp", "fallout.bmp", "nasa.bmp"] + +gc.enable() + +def init_display(): + #print("ePaper init ", str(time.localtime())) + epd = epaper.EPD_5in65() + epd.fill(epd.White) + return epd + +def draw_image(filename): + global epd + global colormap + def color_distance(c1, c2): + def dist(a, b): + return abs(a - b) + return dist(c1[0], c2[0]) + dist(c1[1], c2[1]) + dist(c1[2], c2[2]) + def callback(x, y, color): + global epd + global colormap + best_index = 0 + best_score = 256 + for index in range(len(colormap)): + c = colormap[index] + score = color_distance(c, color) + if score < best_score: + best_score = score + best_index = index + pixel = best_index + #print("PXL ", str([color, r,g,b, pixel])) + epd.pixel(x, y, pixel) + + #print("BMP ", filename, " loading ", str(time.localtime())) + epd.fill(epd.White) + microbmp.MicroBMP(data_callback=callback).load(filename) + #print("BMP loaded ", str(time.localtime())) + epd.EPD_5IN65F_Display(epd.buffer) + #print("ePaper printed ", str(time.localtime())) + + +# MAIN + +epd = init_display() + +while True: + for filename in images: + epd.EPD_5IN65F_Init() + print("TV loading image ", filename) + draw_image(filename) + epd.Sleep() + print("TV showing ", filename) + gc.collect() + epd.delay_ms(10000)