How to Make an E-Paper Quote Display (With Raspberry Pi)
’Tis the holiday season 🎄☃️
Oh boy oh boy.
Time to get together with family & friends, enjoy good meals and exchange some gifts.
Happy times!
If you're still looking for a cool gift for your (nerdy) friend - or just something to treat yourself - I just might have the solution!
A fancy e-paper quote display to display anything you can think of!
Quotes? - check!
Jokes? - double-check!
Pictures of grandma? - triple-granny-check!
Let's dive right in 🤿🐟.
Stuff you’ll need
- 7.5 inch/19cm e-paper display. (I got this one). There are also bigger, higher resolution and multi-colored displays but those tend to get really 💰💰💰, fast.
- Raspberry pi zero 2 W (I got this one). You can also get one with pre-soldered headers if you don't want to scratch your pretty nails.
- Picture frame (I got this one). The screen will be 17X11cm, so you'll just have to find something slightly larger.
- Micro SD card (8GB+ is ideal) for running the OS
- Micro USB cable for power
- 2X20 Pin header + Soldering station/skills (unless you got the pi with pre-soldered headers)
- A nice font to display text in (I used Vollkorn)
Total project cost: +-80€ or about 0,016 camels 🐫.
And we’re off..
Sooo...
The first thing you’ll need to do is setting up your raspberry pi hardware.
You do this by soldering the headers onto your raspberry pi. After that you can just plug the e-Paper Driver HAT on top of your pi and you should be good to go. If you've played with lego before, you can also do this.
The final result should look something like this;
After that’s done you can follow this really handy youtube tutorial to help you set up the software;
Some caveats to this
- You can also add multiple wifi configs to the
wpa_supplicant.conf
file. This way you can move your pi around between the home/main office and it’ll automatically connect there as well.
country=BE
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
scan_ssid=1
ssid="SSID1"
psk="PW1"
}
network={
scan_ssid=1
ssid="SSID2"
psk="PW2"
}
- I also wouldn't do the static ip part if you plan on moving the raspberry pi between locations. You can find the ip address of you pi by;
sudo nmap -sn 192.168.1.0/24
Or just ignore the IP and connect with the default username/password/host combination;
ssh pi@raspberrypi
password: raspberry
Making it come alive 🧟♂️
Once that’s done you can start importing several python example scripts to test the display. The most basic one that you can try out is this repo provided by waveshare electronics themselves.
So you can clone the repo;
git clone https://github.com/waveshare/e-Paper
cd e-Paper/RaspberryPi_JetsonNano/python
And then run some basic install commands for the libraries you'll need;
sudo apt-get update
sudo apt-get install python3-pip
sudo apt-get install python3-pil
sudo apt-get install python3-numpy
sudo pip3 install Jetson.GPIO
After this you’ll be able to run some basic examples in the the examples directory
python examples/epd_7in5_V2_test.py
And if all went well you should see the standard waveshare demo springing to life on your screen. This looks something like this
Congratulations! 🥳
Now - for the fun part - you can start displaying whatever you want on the screen!
I decided I wanted to have some jokes (dark jokes preferably, because I'm a horrible human being) displayed from JokeApi. I basically copied the example script and started from there.
I used the FTP simple VS Code extension to easily change the files on my pi. I added some code to help me with;
- Displaying/resizing images
- Vertically center text
- Handle line-wrapping
- Resize the font if the text is too long
Here's the full (messy) code. Please wash your hands and eyes with soap after use.
#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys
import os
picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic')
mediadir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'media')
libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib')
if os.path.exists(libdir):
sys.path.append(libdir)
import logging
from waveshare_epd import epd7in5_V2 as epd_driver
import time
from PIL import Image,ImageDraw,ImageFont
import traceback
import textwrap
import pdb
import requests
import random
from functools import reduce
import re
import datetime
# Log to output file to see what's going on
logging.basicConfig(level=logging.INFO, filename='/home/pi/e-Paper/src/examples/quotes.log')
# Splits a long text to smaller lines which can fit in a line with max_width.
# Uses a Font object for more accurate calculations
def text_wrap(text, font = None, max_width = None):
lines = []
if font.getsize(text)[0] < max_width:
lines.append(text)
else:
words = text.split(' ')
i = 0
while i < len(words):
line = ''
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
line = line + words[i] + " "
i += 1
if not line:
line = words[i]
i += 1
lines.append(line)
return lines
def slice_index(x):
i = 0
for c in x:
if c.isalpha():
i = i + 1
return i
i = i + 1
def upperfirst(x):
i = slice_index(x)
return x[:i].upper() + x[i:]
# Calculates font-size, line-wrapping, vertical centering, # of lines, strips not-needed parts AND does your dishes
def make_it_pretty(quotes, spacing, screen_height, screen_width, padding):
logging.info("Formatting...")
font_sizes = [64,56,48,40,32,24]
attempt = 0
while True:
attempt += 1
text = random.choice(quotes)
logging.info(f"Quote try {attempt}")
for size in font_sizes:
font = ImageFont.truetype(os.path.join(mediadir, 'vollkorn.ttf'), size)
line_height = font.getsize('hg')[1] + spacing
max_lines = (screen_height // line_height)
splitted_quote = text.split("\n")
result = [text_wrap(part, font = font, max_width = screen_width - (padding * 2)) for part in splitted_quote]
blocks = reduce(lambda x, y: x+y, result)
trimmed_blocks = [x.strip() for x in blocks]
r = re.compile("[\w\"]+")
filtered_list = list(filter(r.match, trimmed_blocks))
line_length = len(filtered_list)
quote_height = line_height * line_length
offset_y = (screen_height / 2) - (quote_height / 2)
if (line_length <= max_lines) and (quote_height + offset_y < screen_height):
quote = upperfirst("\n".join(filtered_list))
logging.info(f"{quote},\n Font size: {size}, Line count: {line_length}, Quote height: {quote_height}, Offset: {offset_y}, Screen height: {screen_height}")
return {
"quote": quote,
"offset": offset_y,
"font": font
}
# Resize PIL image keeping ratio and using white background.
def resize(image, width, height):
ratio_w = width / image.width
ratio_h = height / image.height
if ratio_w < ratio_h:
# It must be fixed by width
resize_width = width
resize_height = round(ratio_w * image.height)
else:
# Fixed by height
resize_width = round(ratio_h * image.width)
resize_height = height
image_resize = image.resize((resize_width, resize_height), Image.ANTIALIAS)
background = Image.new('RGBA', (width, height), (255, 255, 255, 255))
offset = (round((width - resize_width) / 2), round((height - resize_height) / 2))
background.paste(image_resize, offset)
return background.convert('RGB')
def get_jokes():
logging.info("Fetching joke..")
url = "https://v2.jokeapi.dev/joke/Miscellaneous,Dark"
json = requests.get(url).json()
return ["\n".join([json["setup"],json["delivery"]])] if json["type"] == "twopart" else [json["joke"]]
try:
now = datetime.datetime.now()
logging.info(f"\n{now.strftime('%Y-%m-%d %H:%M:%S')}")
logging.info(f"Waking up...")
epd = epd_driver.EPD()
epd.init()
result = get_jokes()
screen_width = epd.width
screen_height = epd.height
line_spacing = 1
padding = 30
view = Image.new('1', (epd.width, epd.height), 255) # 255: clear the frame
draw = ImageDraw.Draw(view)
formatted_result = make_it_pretty(result, line_spacing, screen_height, screen_width, padding)
quote = formatted_result["quote"]
offset_y = formatted_result["offset"]
font = formatted_result["font"]
logging.info("Updating...")
draw.text((padding, offset_y), quote, fill = 0, align = "left", spacing = line_spacing, font = font)
epd.display(epd.getbuffer(view))
logging.info("Standby...")
epd.sleep()
except IOError as e:
logging.info(e)
time.sleep(5)
except KeyboardInterrupt:
logging.info("ctrl + c:")
epd.epdconfig.module_exit()
exit()
Now to finish it up, You can schedule it with a cronjob to update at whatever interval you want. This will update the screen once every hour.
0 * * * * /bin/bash -l -c 'python /home/pi/e-Paper/src/examples/quotes.py'
Now all that's left is to put it in a nice picture frame and hot-glue your raspberry pi to the back.
Aand..
Tadaaa! 🧙🐰🎩 You're done!!
Yes, I'll most definitely will burn in hell for laughing at these...
Totally worth it though.
Recap
It's a cool little desk companion that will give you some additional laughs during the day. Definitely a worthwhile time investment!
Overall it's not that hard to make and a fun little side project to take your mind of other things.
Off-course there's loads of other stuff you can use it for like;
- Slow movie player (https://github.com/TomWhitwell/SlowMovie). Displaying movies at 24 frames per day!
- Images (slideshow for example)
- (Jeroom/xkcd) Cartoons
If you’ve made one as well, let me know in the comments!
Happy holidays!
Comments