Photo of my e-Paper display

I have built an e-paper display for my home to display current information. E-paper is nice, because it does not distract you with a bright surface, and it does not need to be switched on and off.


Most e-paper displays require you to build your own frame and kit around it. Also, many displays are rather small in size.

I found one exception: WaveShare is selling a large (> 12 inch) display, either with two (black/white) or three (black/white/red) colours.

The e-paper comes with a very nice frame, and all you need is to plug in the embedded hardware of your choice to drive it (Raspberry Pi, Arduino or ESP32). I had an old Raspberry Pi 3 lying around, so that was good enough.

The resolution is 1304x984. Power is taken from the Raspberry Pi, make sure that you have a bit spare amps in your power supply.

The only “downside” of the display is the refresh time. A full refresh of the display takes about 20 seconds. At the end of the refresh, the display flickers for a few seconds. As a result, this display is not good for showing information that is changing very often.

I display information that does not change very much during the day, and I refresh the display every hour (if necessary).

System and drivers

I installed a normal Raspbian buster image on a flash card.

WaveShare provides extensive instructions on how to build the basic Python modules.

In the end, you will get a module called epd12in48b that contains the required functions for this display.

The module provides a function called epd12in48b.EPD() that will give you an instance of the display interface. The three main function you need to call are:

  • epd.Init() to initialise the driver and power up the e-paper controller
  • epd.display(black_img, red_img) to actually update the display
  • epd.EPD_Sleep() to power down the controller

In general, you should keep the e-paper powered down between redraws, as this significantly extends the lifetime of the e-paper, as well as saves some energy.

At the same time, the documentation recommends that you refresh the display once per day to prevent a burn-in effect.

This e-paper does not support a partial refresh.

Painting an image

The driver wants two independent monochrome images as offered by the Python PIL (Pillow) libraries, one for red and one for black.

Drawing the two images independently is a hassle, so instead we draw on a normal RGB image and split it into the three colours at the end.

import socket
from PIL import ImageDraw
from PIL import ImageFont
from PIL import ImageColor
from PIL import Image
from PIL import ImageChops


epimage ="RGB", (WIDTH, HEIGHT), "white")
epdraw = ImageDraw.Draw(epimage)
hostname = socket.gethostname()

# Draw something here ...

def split_black_and_red(image):
    palimg ='P', (WIDTH, HEIGHT))
    pal = [255,255,255,255,0,0] + ([0,0,0] * 254)
    palimg = image._new("P", 0,
    rimg = palimg.copy()
    rimg.putpalette([255,255,255,0,0,0] + ([255,255,255] * 254))
    rimg = rimg.convert(mode="1", dither=Image.NONE)
    bimg = palimg.copy()
    bimg.putpalette([255,255,255,255,255,255] + ([0,0,0] * 254))
    bimg = bimg.convert(mode="1", dither=Image.NONE)
    return (bimg,rimg)

if hostname == "raspberrypi":
    import epd12in48b
    epd = epd12in48b.EPD()

The if-else clause at the end allows you to test your script on a local machine, and display the output on your local display. The e-paper is only used when the program is run on the right machine.

A little test

You need a font to display some text. I downloaded the Montserrat TrueType fonts which are very beautiful and have a permissive license.

Here is a small code snippet to display a Hello World text:

from functools import lru_cache

def sans_font(size):
    return ImageFont.truetype("fonts/Montserrat-SemiBold.ttf", size)

epdraw.rectangle([(0,HEIGHT/2-70),(WIDTH,HEIGHT/2+70)], "red")
epdraw.text((WIDTH/2, HEIGHT/2), "Hello World", font=sans_font(150), fill="black", anchor="mm")

Hello World Screenshot

Setting up as a cron job

I am running the script via cron every hour (as root, which is probably not the best thing to do).

# crontab -l
18 * * * * /root/epaper/ > /dev/null

Calling a tiny shell script, just to make sure I am in the right directory and to keep a log file.

set -e
cd /root/epaper
date > last.log
python3 >> last.log

To prevent unnecessary refreshes, I have added a small check to see if anything has changed in the image. If not, there is no need to refresh the e-paper display.

    cacheimg ="cached.png")
    diff = ImageChops.difference(epimage, cacheimg)
    if not diff.getbbox():
except FileNotFoundError:

It would be great to see on the display when it was last updated, otherwise it is hard to tell if the display stopped working.

So, I add a little line at the bottom to show my the current timestamp. This is only added AFTER we check the image for changes.

import datetime
ltime = "Last refresh: %s" %"%Y-%m-%d %H:%M:%S")
epdraw.rectangle([(0, HEIGHT-34), (WIDTH, HEIGHT)], "black")
epdraw.text((WIDTH/2,HEIGHT-10), ltime, fill="white", anchor="mb", font=sans_font(24))

Hello World Screenshot

Next steps

You have plenty of screen estate, so now you can show information that you find interesting. In my case, I added:

  • A ten day forward calendar, including:
    • Upcoming birthdays read from my calendar via caldav.
    • Public bank holidays read from the UK Government Bank Holiday Service
    • My childs school days, manually entered as a static list of start and end dates.
    • Markers for a few personal anniversaries
  • Current COVID numbers for my area and a neighbouring area
  • Weather report for my area
  • Sunrise and sunset times

The result then looks somewhat like this:

My personal e-paper display

Have fun!