Waveshare EPaper and a RaspberryPi

Introduction: Waveshare EPaper and a RaspberryPi

I'm a display nerd, I know. So I got this Waveshare ePaper 2.9" display from ama...n and it was a little nasty to adopt the software, so here is how it went for me.

What to expect:

Some python code for the raspberry to run this display.

  • uses good old PythonImagingLibrary
    • can load any Image
    • allows image manipulation and drawing
  • can fetch images from the internet
  • demo code shows some search results from duckduckgo.com


Here is the waveshare Wiki page with the somewhat useful info's you may need. Unfortunately I am missing info about the memory layout just to know where I can expect to write a bit for a certain pixel. It also lacks timing infos, the code does a lot of delay's and there is a busy pin to read from but when and how I use which ??? The code also contains initialization code that is copied to the display and you will get no glue what it is. And no the PDF document about the display does not reveal this either.

After some experimentation, I can get a refresh rate of about 2fps for a complete image refresh, with the partial update function. The pixels do not completely reset, I guess you need to do the long refresh cycle which inverts everything a view times to get completely clean display back.


Extensive explanation of the Waveshare example code...

Connections to Raspberry

I used a Pi3 but it should work with any.

Search the web for "Raspberry pinout" e.g. this one and match with the waveshare doc, or simply use this table..

e-Paper  RaspberryPi<br>3.3V     3.3V (pin1)<br>GND      GND  (pin6)<br>DIN      MOSI (pin19)<br>CLK      SCLK (pin23)<br>CS       CE0  (pin24)<br>DC       BCM25(pin22)<br>RST      BCM17(pin11)<br>BUSY     BCM24(pin18)

The Code

There is the main.py which runs the fancy stuff and there is EPD_driver.py which is a stripped down version of the interfacing code provided py waveshare. I thrown out almost everything which did not just copy an image to the display - it.. eh.. well they tried, but putting the frame together is much easier done on the Pi and the copied over with a single command.

Just download the attached paperDisp.zip. Then run the code with "python main.py". You will need to install at least PIL and GPIO phyton libs like this.

sudo apt-get install python-requests python-pil python-rpi.gpio

Also make sure the SPI is enabled, edit /boot/config.txt and uncomment the line


main.py - this does init the display, gets some data from a duckduckgo search and displays that along with the current time. (instructables messed up the code, use the attached zip file)

#!/usr/bin/python<br>import spidev as SPI   # where the display connects

import Image, ImageDraw, ImageFont  # PIL - PythonImageLibrary

import time, datetime, sys, signal, urllib, requests
from EPD_driver import EPD_driver
def handler(signum, frame):
    print 'SIGTERM'
signal.signal(signal.SIGTERM, handler)
bus = 0 
device = 0
disp = EPD_driver(spi = SPI.SpiDev(bus, device))
print "disp size : %dx%d"%(disp.xDot, disp.yDot)
print '------------init and Clear full screen------------'
# display part
imagenames = [] 
search = "http://api.duckduckgo.com/?q=Cat&format=json&pretty=1"
if search:
    req = requests.get(search)
    if req.status_code == 200:
        for topic in req.json()["RelatedTopics"]:
            if "Topics" in topic:
                for topic2 in topic["Topics"]:
                        url = topic2["Icon"]["URL"]
                        text = topic2["Text"]
                        if url:
                            imagenames.append( (url,text) )
                        # print topic
                url = topic["Icon"]["URL"]
                if url:
                    imagenames.append( url )
                # print topic
        print req.status_code
# font for drawing within PIL
myfont10 = ImageFont.truetype("amiga_forever/amiga4ever.ttf", 8)
myfont28 = ImageFont.truetype("amiga_forever/amiga4ever.ttf", 28)
# mainimg is used as screen buffer, all image composing/drawing is done in PIL,
# the mainimg is then copied to the display (drawing on the disp itself is no fun)
mainimg = Image.new("1", (296,128))
name = ("images/downloaded.png", "bla")
skip = 0
while 1:
    for name2 in imagenames:
        print '---------------------'
        skip = (skip+1)%7
            starttime = time.time()
            if skip==0 and name2[0].startswith("http"):
                name = name2
                urllib.urlretrieve(name[0], "images/downloaded.png")
                name = ("images/downloaded.png", name2[1])
            im = Image.open(name[0])
            print name, im.format, im.size, im.mode
            im = im.convert("1") #, dither=Image.NONE)
            # print 'thumbnail', im.format, im.size, im.mode
            loadtime = time.time()
            print 't:load+resize:', (loadtime - starttime)
            draw = ImageDraw.Draw(mainimg)
            # clear
            draw.rectangle([0,0,296,128], fill=255)
            # copy to mainimg
            ypos = (disp.xDot - im.size[1])/2
            xpos = (disp.yDot - im.size[0])/2
            print 'ypos:', ypos, 'xpos:', xpos
            mainimg.paste(im, (xpos,ypos))
            # draw info text
            ts = draw.textsize(name[1], font=myfont10)
            tsy = ts[1]+1
            oldy = -1
            divs = ts[0]/250
            for y in range(0, divs):
                newtext = name[1][(oldy+1)*len(name[1])/divs:(y+1)*len(name[1])/divs]
                # print divs, oldy, y, newtext
                oldy = y
                draw.text((1, 1+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((1, 3+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((3, 3+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((3, 1+y*tsy), newtext, fill=255, font=myfont10)
                draw.text((2, 2+y*tsy), newtext, fill=0, font=myfont10)
            #draw time
            now = datetime.datetime.now()
            tstr = "%02d:%02d:%02d"%(now.hour,now.minute,now.second)
            # draw a shadow, time
            tpx = 36
            tpy = 96
            for i in range(tpy-4, tpy+32, 2):
                draw.line([0, i, 295, i], fill=255)
            draw.text((tpx-1, tpy  ), tstr, fill=0, font=myfont28)
            draw.text((tpx-1, tpy-1), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy-1), tstr, fill=0, font=myfont28)
            draw.text((tpx+2, tpy  ), tstr, fill=0, font=myfont28)
            draw.text((tpx+2, tpy+2), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy+2), tstr, fill=0, font=myfont28)
            draw.text((tpx  , tpy  ), tstr, fill=255, font=myfont28)
            del draw
            im = mainimg.transpose(Image.ROTATE_90)
            drawtime = time.time()
            print 't:draw:', (drawtime - loadtime)
            listim = list(im.getdata())
            # print im.format, im.size, im.mode, len(listim)
            listim2 = []
            for y in range(0, im.size[1]):
                for x in range(0, im.size[0]/8):
                    val = 0
                    for x8 in range(0, 8):
                        if listim[(im.size[1]-y-1)*im.size[0] + x*8 + (7-x8)] > 128:
                            # print x,y,x8,'ON'
                            val = val | 0x01 << x8
                            # print x,y,x8,'OFF'
                    # print val
            for x in range(0,1000):
            # print len(listim2)
            convtime = time.time()
            print 't:conv:', (convtime - loadtime)
            ypos = 0
            xpos = 0
            disp.EPD_Dis_Part(xpos, xpos+im.size[0]-1, ypos, ypos+im.size[1]-1, listim2) # xStart, xEnd, yStart, yEnd, DisBuffer
            # disp.delay()
            uploadtime = time.time()
            print 't:upload:', (uploadtime - loadtime)
        except IOError as ex:
            print 'IOError', str(ex)</p>


I used the free font "Amiga Forever" by Freaky Fonts http://www.dafont.com/amiga-forever.font .

Images shown on the disp are search results from duckduckgo "cat" search, no preferences for whatever comes up there.

Be the First to Share


    • Fruits and Veggies Speed Challenge

      Fruits and Veggies Speed Challenge
    • Make It Modular: Student Design Challenge

      Make It Modular: Student Design Challenge
    • Pets Challenge

      Pets Challenge



    Question 2 years ago

    Hi, i seem to have a problem after loading the screen content. There is a grey border appearing slowly and staying there. It also disrupts the screen content as seen on the picture. Do you think it is fixable somehow. It happened after running the waveshare code maybe three times on a new display and persists after adding some own code. I hope its not permanent damage like you mentioned in the video. Thank you very much!


    3 years ago on Introduction

    Ok I got this working with RPI Zero W without many modification, only PIL import line that was already mentioned. Thank you very much. A thing to note is that my screen has a bit of ghosting around the contrasting elements after some updates. Two flashes one white one brack clear the problem so it is not persistent.

    One issue I see is that we could have solved is that it works with python 2.7 and not 3.x when some off new helper libraries I'd love to use in my sensor project are 3.x exclusive

    Added a wiring diagram for my project, as the table on the main page is hard to read. Waveshare screen comes with the colorful leads. Valid until they change colors :) (uses other ground connector for better packing)

    2018-10-15 15.10.55.jpg2018-12-20 14_55_25-CorelDRAW 2018 (64-Bit) - C__Users_sherl_Dropbox_EUI sensor_rpi design.cdr.png
    On The 3dge
    On The 3dge

    Reply 3 years ago

    Hi MaciejE2, Don't suppose you still have the code for your project. I'd love to take a look at it, Attempting a similar project myself but with temp and pressure for a weather station.

    Alfred Nobel
    Alfred Nobel

    Question 3 years ago

    Hi, I followed the instructions (step by step), but I get this Error (screenshot also included ):

    Reset is complete
    disp size : 128x296
    ------------init and Clear full screen------------
    1.init full screen
    2.clear full screen
    Traceback (most recent call last):
    File "/home/pi/Schreibtisch/neu0/RaspberryPi/python1/main.py", line 95, in <module>
    myfont10 = ImageFont.truetype("amiga_forever/amiga4ever.ttf", 8)
    File "/usr/lib/python2.7/dist-packages/PIL/ImageFont.py", line 238, in truetype
    return FreeTypeFont(font, size, index, encoding)
    File "/usr/lib/python2.7/dist-packages/PIL/ImageFont.py", line 127, in __init__
    self.font = core.getfont(font, size, index, encoding)
    IOError: cannot open resource

    Any Idea what the problem could be?


    Answer 3 years ago

    Looks like the font is not found. It is searched under "amiga_forever" from your current path. Make sure you have that or make it an absolut path or copy the ttf file to /usr/share/fonts and remove the path in the code..
    Cheers M.

    Alfred Nobel
    Alfred Nobel

    Reply 3 years ago

    Thanks this was very helpful :)
    But now I've another problem : This time I get a number of x entries for every search but it takes everytime ages to continue with the next "search-word" and nothing is displayed on the ePaper-display

    A screenshot is added (by the way i waited half an hour till I got to this point and it is still working with new "search results"/x entries poping up)

    screenshot ePaper.png
    Alfred Nobel
    Alfred Nobel

    Reply 3 years ago

    Any Idea ? The program got stuck in this continuos loop and the display seems not to be activated :( What could've gone wrong ?


    Reply 3 years ago

    Hard to say, try to comment in some of the print command at the end of main.py and find out where all the time goes.

    Alfred Nobel
    Alfred Nobel

    Reply 3 years ago

    I commented in the 'print drawtime, convtime and uploadtime' but I got the error "uploadtime not defined" so I added the line "uploadtime = time.time()" then this error disappeared but the program still got stuck emitting search results eternally (with the draw-,conv-,uploadtimes being printed now). The display still shows no Image.


    Question 3 years ago on Introduction

    I received a pdf file that may be of some help to you, but it's over my head (sadly that doesn't take much anymore). If you would like a copy please tell me how to get it to you.


    Question 4 years ago on Introduction

    In the EPD_driver.py file, where did you get the values for LUTDefault_full and LUTDefault_part? I'm trying to modify your code to drive the 7.5" Waveshare display, but I'm not seeing anything equivalent to that data in the original Waveshare code. I see that same data in a few different Github repos, but no explanation as to where it comes from. Thanks.


    Answer 3 years ago

    I am currently having the same issue.

    Did you find any solution yet?


    Reply 3 years ago

    I don't believe I ever found an explanation for those two values, however I did get my display running by building off the code supplied by Waveshare. You can dig through my project on Github to see how I made it work: https://github.com/kdickerson/homeBoard


    Question 4 years ago on Introduction

    Hey there, nice tutorial! I got myself a 2,7" waveshare HAT and wonder at which location you got the original EPD_Driver.py? My display seems to be not supported... that's not what I expected when buying a HAT for my Raspberry Pi3


    Answer 4 years ago

    Hi, The waveshare wiki has a page for each display there is a link to a demo zip file on this page.

    Cheers, M.


    Question 4 years ago

    I'm having a python issue with the waveshare and this example.

    I followed the instructions using the latest raspbian stretch, and installed python-pil

    Traceback (most recent call last):

    File "main.py", line 3, in <module>

    import Image, ImageDraw, ImageFont # PIL - PythonImageLibrary

    ImportError: No module named Image

    Also tried to install pil and then pillow with pip, but no luck.

    Anyone have the same issue?


    Answer 4 years ago

    In case anyone else wonders the same.

    Seems like the problem with at least the waveshare example was that

    "import Image" needed to be "from PIL import Image"

    same for "ImageFont" and "ImageDraw" which also needed the "from PIL" part.


    Question 4 years ago on Introduction

    do you have full command list what i need to install etc. I have clean raspberry pi zero w with rasbian lite. i try earlier and error is no module named EPD_driver


    Answer 4 years ago

    Look for the attached zip file, this contains my code, main.py and the modified EPD_driver.py (original was from Waveshare download) Eventually your python does not look for lib files in the current directory -> ask your fav search engine.


    4 years ago

    Hi, it's difficult to "fix" the software without the hardware for testing it, It took me a some iterations to get mine running.
    However, the docu of the 2.13 HAT says something about a "virtual width" of 128 instead of 122, meaning the display memory is 128 pixel width to keep the lines byte aligned.
    Maybe a width setting of 128 does the trick.