Skip to content

Instantly share code, notes, and snippets.

@c3c
Created June 8, 2024 20:26
Show Gist options
  • Save c3c/b7f3631f945ad337d8f416adef311a44 to your computer and use it in GitHub Desktop.
Save c3c/b7f3631f945ad337d8f416adef311a44 to your computer and use it in GitHub Desktop.
Snipppet for a T8-C3 driving a WaveShare 7.3in ACeP epaper display
# Snipppet for a T8-C3 driving a WaveShare 7.3in ACeP epaper display
# Retrieves pictures from wifi and stream them to the display
# Pictures must be preformatted in GS4_HMSB format
# Updates screen every 4 hours with a new picture
from machine import deepsleep, Pin, SPI
import time
import network
import requests
import random
import esp32
import gc
import struct
SLEEPTIME = 4*60*60 * 1000
PIN_CS = 10
PIN_MOSI = 7
PIN_SCK = 6
PIN_BUSY = 5
PIN_DIN = 7
PIN_DC = 4
PIN_RST = 8
class Epaper73():
def __init__(self):
self.rst_pin = Pin(PIN_RST, Pin.OUT)
self.busy_pin = Pin(PIN_BUSY, Pin.IN, Pin.PULL_UP)
self.cs_pin = Pin(PIN_CS, Pin.OUT)
self.spi = SPI(1)
self.spi.init(baudrate=1_000_000)
self.dc_pin = Pin(PIN_DC, Pin.OUT)
self.width = 800
self.height = 480
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.Epaper73_Init()
def delay_ms(self, delaytime):
time.sleep_ms(delaytime)
def spi_writebyte(self, data):
self.spi.write(bytearray(data))
def digital_read(self, pin):
return pin.value()
def digital_write(self, pin, value):
pin.value(value)
def reset(self):
self.digital_write(self.rst_pin, 1)
self.delay_ms(20)
self.digital_write(self.rst_pin, 0)
self.delay_ms(5)
self.digital_write(self.rst_pin, 1)
self.delay_ms(20)
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(10)
def BusyLow(self):
while(self.digital_read(self.busy_pin) == 1):
self.delay_ms(10)
def Epaper73_Init(self):
self.reset()
self.BusyHigh()
self.delay_ms(30)
self.send_command(0xaa)
self.send_data(0x49)
self.send_data(0x55)
self.send_data(0x20)
self.send_data(0x08)
self.send_data(0x09)
self.send_data(0x18)
self.send_command(0x01)
self.send_data(0x3f)
self.send_data(0x00)
self.send_data(0x32)
self.send_data(0x2a)
self.send_data(0x0e)
self.send_data(0x2a)
self.send_command(0x00)
self.send_data(0x5f)
self.send_data(0x69)
self.send_command(0x03)
self.send_data(0x00)
self.send_data(0x54)
self.send_data(0x00)
self.send_data(0x44)
self.send_command(0x05)
self.send_data(0x40)
self.send_data(0x1f)
self.send_data(0x1f)
self.send_data(0x2c)
self.send_command(0x06)
self.send_data(0x6f)
self.send_data(0x1f)
self.send_data(0x1f)
self.send_data(0x22)
self.send_command(0x08)
self.send_data(0x6f)
self.send_data(0x1f)
self.send_data(0x1f)
self.send_data(0x22)
self.send_command(0x13)
self.send_data(0x00)
self.send_data(0x04)
self.send_command(0x30)
self.send_data(0x3c)
self.send_command(0x41)
self.send_data(0x00)
self.send_command(0x50)
self.send_data(0x3f)
self.send_command(0x60)
self.send_data(0x02)
self.send_data(0x00)
self.send_command(0x61)
self.send_data(0x03)
self.send_data(0x20)
self.send_data(0x01)
self.send_data(0xe0)
self.send_command(0x82)
self.send_data(0x1e)
self.send_command(0x84)
self.send_data(0x00)
self.send_command(0x86)
self.send_data(0x00)
self.send_command(0xe3)
self.send_data(0x2f)
self.send_command(0xe0)
self.send_data(0x00)
self.send_command(0xe6)
self.send_data(0x00)
def Epaper73_Clear(self, color):
self.send_command(0x10)
for _ in range(0, int(self.width // 2)):
self.send_data1([color<<4 | color] * self.height)
self.Epaper73_TurnOnDisplay()
self.delay_ms(500)
def Epaper73_Display(self, pic_stream):
self.send_command(0x10)
for _ in range(self.width // 2):
bytes_read = pic_stream.read(self.height)
self.send_data1(bytes_read)
self.Epaper73_TurnOnDisplay()
self.delay_ms(200)
def Epaper73_TurnOnDisplay(self):
self.send_command(0x04)
self.BusyHigh()
self.send_command(0x12)
self.send_data(0x00)
self.BusyHigh()
self.send_command(0x02)
self.send_data(0x00)
self.BusyLow()
def Epaper73_Sleep(self):
self.delay_ms(100);
self.send_command(0x07);
self.send_data(0xA5);
self.delay_ms(100);
self.digital_write(self.rst_pin, 1)
led = Pin(3, Pin.OUT)
led.value(1)
time.sleep(1)
led.value(0)
nvs = esp32.NVS("photo")
def connect_wifi():
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect('wifinetwork', 'wifipassword')
tries = 0
while tries < 10 and not sta_if.isconnected():
time.sleep(1)
tries += 1
if not sta_if.isconnected():
raise Exception("wifi fail")
def set_nvs_last(x):
nvs.set_blob(b"last", x.encode())
def get_nvs_last():
x = bytearray(256)
try:
nvs.get_blob(b"last", x)
except OSError:
return b""
return x.strip(b"\x00").decode()
def get_timed_file_list():
file_list = requests.get("http://epaper.your.lan/epaper.txt") # list of files, new line separated
assert file_list.status_code == 200
hour = int(file_list.headers['Date'].strip().split(' ')[-2].split(':')[0])
pics = file_list.text.strip().splitlines()
return hour, pics
def select_next_pic(pic_list):
last_pic = get_nvs_last()
filtered = list(filter(lambda b: b!=last_pic, pic_list))
return random.choice(filtered)
def get_picture_stream(the_pic):
gs4 = the_pic.replace(".bmp", ".gs4") # obtain gs4 formatted picture (GS4_HMSB)
pic = requests.get(f"http://epaper.your.lan/{gs4}", stream=True)
assert pic.status_code == 200
raw_len = int(pic.headers['Content-Length'])
assert raw_len == 192000
return pic.raw
quiet_time = False
try:
print("[ ] Wifi connecting")
connect_wifi()
print("[+] Wifi connected")
print("[ ] Pic list fetch")
hour, pic_list = get_timed_file_list()
print("[+] Pic list retrieved: %d pics" % len(pic_list))
quiet_time = hour >= 20
print("[+] Time = %dh GMT. Quiet time starting: %s" % (hour, str(quiet_time)))
next_pic = select_next_pic(pic_list)
print("[*] Next up: %s" % next_pic)
set_nvs_last(next_pic)
print("[+] Next pic in NVS")
print("[ ] Fetching picture")
pic_stream = get_picture_stream(next_pic)
print("[+] Fetching picture OK")
ep = Epaper73()
ep.Epaper73_Clear(0x01)
ep.Epaper73_Display(pic_stream)
ep.Epaper73_Sleep()
print("[+] Done")
except Exception as e:
print(e)
pass
if quiet_time:
deepsleep(SLEEPTIME*2)
else:
deepsleep(SLEEPTIME)
@c3c
Copy link
Author

c3c commented Jun 8, 2024

Following scripts can be used to convert images to gs4 format, improve brightness/contrast/saturation, with optional rotation.

First convert any input image to bmp with palette (this one is WaveShare's):

#encoding: utf-8

import sys
import os.path
from PIL import Image, ImagePalette, ImageOps
import argparse

# Create an ArgumentParser object
parser = argparse.ArgumentParser(description='Process some images.')

# Add orientation parameter
parser.add_argument('image_file', type=str, help='Input image file')
parser.add_argument('--dir', choices=['landscape', 'portrait'], help='Image direction (landscape or portrait)')
parser.add_argument('--mode', choices=['scale', 'cut'], default='scale', help='Image conversion mode (scale or cut)')
parser.add_argument('--dither', type=int, choices=[Image.NONE, Image.FLOYDSTEINBERG], default=Image.FLOYDSTEINBERG, help='Image dithering algorithm (NONE(0) or FLOYDSTEINBERG(3))')

# Parse command line arguments
args = parser.parse_args()

# Get input parameter
input_filename = args.image_file
display_direction = args.dir
display_mode = args.mode
display_dither = Image.Dither(args.dither)

# Check whether the input file exists
if not os.path.isfile(input_filename):
    print(f'Error: file {input_filename} does not exist')
    sys.exit(1)

# Read input image
input_image = Image.open(input_filename)

# Get the original image size
width, height = input_image.size

# Specified target size
if display_direction:
    if display_direction == 'landscape':
        target_width, target_height = 800, 480
    else:
        target_width, target_height = 480, 800
else:
    if  width > height:
        target_width, target_height = 800, 480
    else:
        target_width, target_height = 480, 800
    
if display_mode == 'scale':
    # Computed scaling
    scale_ratio = max(target_width / width, target_height / height)

    # Calculate the size after scaling
    resized_width = int(width * scale_ratio)
    resized_height = int(height * scale_ratio)

    # Resize image
    output_image = input_image.resize((resized_width, resized_height))

    # Create the target image and center the resized image
    resized_image = Image.new('RGB', (target_width, target_height), (255, 255, 255))
    left = (target_width - resized_width) // 2
    top = (target_height - resized_height) // 2
    resized_image.paste(output_image, (left, top))
elif display_mode == 'cut':
    # Calculate the fill size to add or the area to crop
    if width / height >= target_width / target_height:
        # The image aspect ratio is larger than the target aspect ratio, and padding needs to be added on the left and right
        delta_width = int(height * target_width / target_height - width)
        padding = (delta_width // 2, 0, delta_width - delta_width // 2, 0)
        box = (0, 0, width, height)
    else:
        # The image aspect ratio is smaller than the target aspect ratio and needs to be filled up and down
        delta_height = int(width * target_height / target_width - height)
        padding = (0, delta_height // 2, 0, delta_height - delta_height // 2)
        box = (0, 0, width, height)

    resized_image = ImageOps.pad(input_image.crop(box), size=(target_width, target_height), color=(255, 255, 255), centering=(0.5, 0.5))


# Create a palette object
pal_image = Image.new("P", (1,1))
pal_image.putpalette( (0,0,0,  255,255,255,  0,255,0,   0,0,255,  255,0,0,  255,255,0, 255,128,0) + (0,0,0)*249)
  
# The color quantization and dithering algorithms are performed, and the results are converted to RGB mode
quantized_image = resized_image.quantize(dither=display_dither, palette=pal_image).convert('RGB')

# Save output image
output_filename = os.path.splitext(input_filename)[0] + '_' + display_mode + '_output.bmp'
quantized_image.save(output_filename)

print(f'Successfully converted {input_filename} to {output_filename}')

Post process then using:

import glob
import struct
from PIL import Image, ImageEnhance
from io import BytesIO

for g in glob.glob("*.bmp"):
	print(g)

	im = Image.open(g)
	rot = im.rotate(180)
	rot = rot.transpose(Image.FLIP_LEFT_RIGHT)

	enhancer = ImageEnhance.Brightness(rot)
	img_brightened = enhancer.enhance(1.2)
        
	enhancer = ImageEnhance.Contrast(img_brightened)
	img_contrasted = enhancer.enhance(1.5)

	enhancer = ImageEnhance.Color(img_contrasted)
	img_res = enhancer.enhance(1.5)

	bio = BytesIO()
	#img_res.save(g + "_modded.bmp")
	img_res.save(bio, format='BMP')
	bmp = bio.getvalue()

	bmp_header = bmp[:54]

	offset = struct.unpack('<i', bmp_header[10:10+4])[0]
	width, height = struct.unpack('<ii', bmp_header[18:18+8])
	bpp = struct.unpack('<H', bmp_header[28:28+2])[0]
	assert all([width == 800, height == 480, bpp == 24, offset == 54])

	pic_stream = BytesIO(bmp[54:])
	
	with open(g[:-3] + "gs4","wb") as buffer_file:
		for w in range(800):
			rowdata = pic_stream.read(480 * 3)
                
			for cc in range(0, 480*3, 3):
				cdata = rowdata[cc:cc+3]
				color = 0
				if cdata == b"\x00\x00\x00":
					color = 0
				elif cdata == b"\xff\xff\xff":
					color = 1
				elif cdata == b"\x00\xff\x00":
					color = 2
				elif cdata == b"\xff\x00\x00":
					color = 3
				elif cdata == b"\x00\x00\xff":
					color = 4
				elif cdata == b"\x00\xff\xff":
					color = 5
				elif cdata == b"\x00\x80\xff":
					color = 6
                    
				if cc % 2 == 0:
					color_prev = color
				else:
					# todo: should this be color, then color_prev or the other way around?
					buffer_file.write(bytes([color_prev << 4 | color]))   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment