Initial version - cycling of BMP images

This commit is contained in:
Dejvino 2023-04-19 20:04:02 +02:00
commit c6ce9c6654
4 changed files with 770 additions and 0 deletions

309
epaper.py Normal file
View File

@ -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")

11
gallery/convert_image.sh Executable file
View File

@ -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"

382
microbmp.py Normal file
View File

@ -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("<I", data[2:6])[0]
self.BMP_reserved1 = data[6:8]
self.BMP_reserved2 = data[8:10]
self.BMP_offset = unpack("<I", data[10:14])[0]
# DIB Header
data = bf_io.read(4)
self.DIB_len = unpack("<I", data[0:4])[0]
data = bf_io.read(self.DIB_len - 4)
(
self.DIB_w,
self.DIB_h,
self.DIB_planes_num,
self.DIB_depth,
self.DIB_comp,
self.DIB_raw_size,
self.DIB_hres,
self.DIB_vres,
) = unpack("<iiHHIIii", data[0:28])
print("BMP metadata: ", str([
self.DIB_w,
self.DIB_h,
self.DIB_planes_num,
self.DIB_depth,
self.DIB_comp,
self.DIB_raw_size,
self.DIB_hres,
self.DIB_vres,
]))
DIB_plt_num_info = unpack("<I", data[28:32])[0]
DIB_plt_important_num_info = unpack("<I", data[32:36])[0]
if self.DIB_len > 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("<I", self.BMP_size))
bf_io.write(self.BMP_reserved1)
bf_io.write(self.BMP_reserved2)
bf_io.write(pack("<I", self.BMP_offset))
# DIB Header
bf_io.write(
pack(
"<IiiHHIIiiII",
self.DIB_len,
self.DIB_w,
self.DIB_h,
self.DIB_planes_num,
self.DIB_depth,
self.DIB_comp,
self.DIB_raw_size,
self.DIB_hres,
self.DIB_vres,
self.DIB_num_in_plt,
self.DIB_num_in_plt,
)
)
if self.DIB_len > 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

68
test.py Normal file
View File

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