E-Paper Status Display on Pi Zero with a Waveshare 2.13 inch HAT

A headless Pi Zero gives no hint of what it is doing. Is it connected? What is its IP address? Is it overheating? A tiny e-paper display answers those questions at a glance. E-paper, also called e-ink, is the same technology used in e-book readers: it reflects ambient light, is readable in sunlight, and keeps showing the last image even with the power removed, drawing current only while it changes.

The Waveshare 2.13 inch e-Paper HAT is sized to sit exactly on top of a Pi Zero. In this tutorial you will enable SPI, install the Waveshare Python driver, and write a program that draws the hostname, IP address, CPU temperature and update time with the Pillow imaging library, refreshing every few minutes.

How E-Paper Differs from LCD and OLED

Each pixel of an e-paper panel contains charged black and white particles suspended in fluid. Applying a voltage moves one colour to the surface. Once moved, the particles stay put, so a static image needs no power. The trade-off is speed: a full refresh takes a couple of seconds and flashes the screen black and white to clear ghosting, which makes e-paper unsuitable for animation but ideal for dashboards, labels and name badges.

PropertyE-paperOLED (e.g. SSD1306)
Power for a static imageNoneContinuous
Readable in direct sunlightYesPoor
Refresh speedSlow, seconds for a full refreshFast, suitable for animation
Image kept after power offYesNo
Best useStatus, labels, slowly changing dataMenus, live readings, graphics

The 2.13 inch panel has a resolution of 250 x 122 pixels in black and white. Waveshare has sold several revisions (V2, V3, V4) that need different driver files; the revision is printed on the back of the board, and you must import the matching module.

Connections and Software Setup

The HAT plugs straight onto the 40-pin header, so no wiring is needed if your Zero has a header fitted. If you use the bare module with its cable instead, connect it as follows.

Display pinFunctionPi Zero pin (physical)BCM name
VCC3.3 V supplyPin 13.3 V
GNDGroundPin 6GND
DINSPI data inPin 19GPIO10 (MOSI)
CLKSPI clockPin 23GPIO11 (SCLK)
CSChip selectPin 24GPIO8 (CE0)
DCData / command selectPin 22GPIO25
RSTResetPin 11GPIO17
BUSYBusy output from panelPin 18GPIO24
Enable SPI, install dependencies and fetch the Waveshare driver
# Enable the SPI interface (0 = enable) and reboot
sudo raspi-config nonint do_spi 0
sudo reboot

# After reboot: SPI device files should exist
ls /dev/spidev0.*

# Python libraries from the OS repository
sudo apt install -y git python3-pil python3-numpy python3-spidev python3-gpiozero fonts-dejavu-core

# Waveshare's driver collection (only the lib folder is needed)
git clone --depth 1 https://github.com/waveshare/e-Paper.git ~/e-Paper
ls ~/e-Paper/RaspberryPi_JetsonNano/python/lib/waveshare_epd/ | grep epd2in13

The Status Display Program

Save this as epaper_status.py. Change epd2in13_V4 to the module that matches your board revision, such as epd2in13_V3.

epaper_status.py: draw system status on a 2.13 inch e-paper HAT every 5 minutes
#!/usr/bin/env python3
"""Show Pi Zero status on a Waveshare 2.13 inch e-paper HAT."""
import os
import socket
import subprocess
import sys
import time
from datetime import datetime

# Make the Waveshare library importable from the cloned repository
sys.path.append(os.path.expanduser('~/e-Paper/RaspberryPi_JetsonNano/python/lib'))
from waveshare_epd import epd2in13_V4 as epd_driver  # match your board revision
from PIL import Image, ImageDraw, ImageFont

REFRESH_S = 300  # 5 minutes; frequent full refreshes shorten panel life
FONT_PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'

def load_font(size):
    try:
        return ImageFont.truetype(FONT_PATH, size)
    except OSError:
        return ImageFont.load_default()  # fallback if the font is missing

BIG, SMALL = load_font(20), load_font(14)

def ip_address():
    # hostname -I lists all addresses; take the first one
    out = subprocess.run(['hostname', '-I'], capture_output=True, text=True).stdout.split()
    return out[0] if out else 'no network'

def cpu_temp():
    with open('/sys/class/thermal/thermal_zone0/temp') as f:
        return int(f.read()) / 1000

def draw_status(epd):
    # Landscape canvas: width = epd.height (250), height = epd.width (122)
    image = Image.new('1', (epd.height, epd.width), 255)  # 255 = white
    draw = ImageDraw.Draw(image)
    draw.text((4, 2), socket.gethostname(), font=BIG, fill=0)
    draw.line((0, 28, epd.height, 28), fill=0, width=2)
    draw.text((4, 34), f'IP:   {ip_address()}', font=SMALL, fill=0)
    draw.text((4, 54), f'CPU:  {cpu_temp():.1f} C', font=SMALL, fill=0)
    draw.text((4, 74), f'Load: {os.getloadavg()[0]:.2f}', font=SMALL, fill=0)
    draw.text((4, 100), datetime.now().strftime('Updated %d %b %H:%M'), font=SMALL, fill=0)
    return image

def main():
    epd = epd_driver.EPD()
    try:
        while True:
            epd.init()                         # wake the panel
            epd.display(epd.getbuffer(draw_status(epd)))
            epd.sleep()                        # low-power mode; image stays visible
            time.sleep(REFRESH_S)
    except KeyboardInterrupt:
        pass
    finally:
        epd_driver.epdconfig.module_exit(cleanup=True)  # release SPI and GPIO

if __name__ == '__main__':
    main()

How the Program Works

  • Pillow does the drawing: the program builds a 1-bit image in memory with text and lines, then getbuffer converts it into the byte layout the panel controller expects, including rotation.
  • Init, display, sleep: the panel is woken only to update and put back to sleep straight away. Waveshare advises not leaving the panel powered in a high-voltage state for long periods.
  • Landscape canvas: the driver reports the panel as 122 wide and 250 high, so the image is created with the two swapped to draw in landscape.
  • Refresh interval: e-paper panels are rated for a limited number of refreshes, and full refreshes are slow. Five minutes suits a status display. The V3 and V4 drivers also offer partial refresh for small changes such as a clock, using displayPartBaseImage and displayPartial.

Run the program as a systemd service so the display updates after every boot, which also means it shows the new IP address whenever the Zero joins a different network. That alone makes headless field use much easier.

Planning the layout on paper first saves many slow refresh cycles. At 250 x 122 pixels, a 20-pixel font fits about four lines with spacing, and a 14-pixel font about six. Pillow's draw.textbbox((0, 0), text, font=font) returns the bounding box of a string, which you can use to right-align values or check that a long IP address or hostname does not run off the edge before sending the image to the panel. While developing, call image.save('preview.png') and copy the file to your laptop to check the design without waiting for the display.

Troubleshooting

  • Program hangs at init: the BUSY pin never goes idle. Check the HAT is fully seated, SPI is enabled and you imported the correct revision module.
  • Garbled or shifted image: wrong driver revision. Compare the label on the board with the module name.
  • No such file /dev/spidev0.0: SPI is not enabled, or you did not reboot after enabling it.
  • Faint ghost of the previous image: normal after many partial refreshes. Do a full refresh with epd.init() and epd.Clear(0xFF) occasionally.
  • Permission errors: your user must be in the spi and gpio groups; default users already are.

Summary

An e-paper HAT gives a headless Pi Zero a sunlight-readable display that uses no power between updates. Enable SPI, use the Waveshare driver that matches your panel revision, draw with Pillow, and always follow the init, display, sleep sequence. The same approach works for weather summaries, MQTT readings or a desk name badge.