How to Make an E-paper Quote Display (With Raspberry Pi)

 - Webdesign Antwerpen

’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;

If you’ve made one as well, let me know in the comments!

Happy holidays!

Resources


Questions/Suggestions?

Related Articles

 - Webdesign Antwerpen

How to Upload Subscribers to Mailchimp Using CSV File (RubyShorts)

Ever wanted to bulk upload users to your mailchimp account but were hindered because of the omnivore alert? Well with some magical ruby code and an API-key you won't have any problems :)

 - Webdesign Antwerpen

How To Install OneNote On Ubuntu (2017)

Do you love keeping your notes in OneNote, made by Microsoft and can't really live without it? Well in this video I'm going to show you how you can set it up on your linux ubuntu device.

 - Webdesign Antwerpen

How To Create An Automatic Sitemap For Your Rails App On Heroku (RailsShorts)

Wish your sitemap was automatically updated once a week or faster without having to manually update it and push the changes to your server? Combine sitemap generator & fog to fix this!

 - Webdesign Antwerpen

How To Do Basic CSV Manipulations In Ruby (RubyShorts)

Need some basic stuff done on your CSV like creating, reading, writing or appending? Here's a short overview!

 - Webdesign Antwerpen

How To Handle Errors In Ruby With Begin, Rescue & Ensure (RubyShorts)

Are your trying to catch some errors in your ruby application but can't really wrap your head around the begin, rescue and ensure blocks in ruby? Here are some pointers!

 - Webdesign Antwerpen

How To Query A Basic API In Ruby (RubyShorts)

Here's a quick article on how you can quickly retrieve data from an API endpoint using the open-uri and json library