Display images on a 13.3" Spectra 6 color e-ink display using custom firmware for the Seeed Studio XIAO ePaper Display Board (EE02).
Replaces the Seeed factory-installed firmware on the EE02 board with custom firmware that:
- Connects to your WiFi network
- Wakes up from deep sleep to fetch an image from an image server that you can run locally or on a remote server
- Displays the image on a spectra 6 eink screen
- Goes back to sleep to conserve battery (can vary sleep interval)
- Skips wakeups during configurable quiet hours (like overnight when no one is seeing the display)
- Wakes up periodically to check for new images and only refreshes the image if it has changed
- Python 3.10 or newer
uv- A Seeed EE02 / XIAO ePaper board with a 13.3" Spectra 6 panel
- A USB-C data cable
- A 2.4 GHz WiFi network
If you do not already have uv, install it with:
curl -LsSf https://astral.sh/uv/install.sh | shgit clone <repository-url>
cd seeed_eink_boardOr download and extract the ZIP file from the repository.
This installs Python packages and PlatformIO:
uv syncThis may take a few minutes the first time as it downloads PlatformIO and the ESP32 toolchain.
Copy the example config file and edit it with your credentials:
cd firmware/src
cp config.h.example config.hEdit config.h and change these lines to match your WiFi network:
#define WIFI_SSID "YourNetworkName"
#define WIFI_PASSWORD "YourPassword"Note:
- The ESP32 only supports 2.4GHz WiFi (not 5GHz)
- Make sure to keep the quotes around the values
The board needs the IP address of the computer that will run image_server.py.
Linux:
hostname -ImacOS:
ipconfig getifaddr en0The address can be on your local network or a remote server depending on where you want to run the image server.
Assuming at least for testing purposes you are running the image server from this repository. Of course you can run it from wherever you like.
Edit firmware/src/config_manager.h:
Find this line and change the IP address to your server's IP:
#define DEFAULT_SERVER_HOST "192.168.86.33" // Change this to your image server's IP addressThe other settings should be fine:
DEFAULT_SERVER_PORT 5000- The server runs on port 5000DEFAULT_IMAGE_ENDPOINT "/image_packed"- The URL path for imagesDEFAULT_SLEEP_MINUTES 15- minutes between wakeups during active hoursDEFAULT_ACTIVE_START_HOUR 8- local hour when normal refreshes beginDEFAULT_ACTIVE_END_HOUR 20- local hour when quiet hours beginDEFAULT_TIMEZONE_OFFSET_MINUTES 0- minutes from UTC, for example-300for EST without DST
The device can also pull these schedule settings from the server later, so this default only has to be good enough to get you started.
cd firmware
uv run pio runThe first build takes several minutes as it downloads the ESP32 compiler and libraries. Subsequent builds are much faster.
You should see:
========================= [SUCCESS] Took XX.XX seconds =========================
- Connect the EE02 board to your computer using a USB-C cable
- Note: If nothing seems to be happening, your cable might be charge-only.
Check if you can see the device:
Linux:
ls /dev/ttyACM*macOS:
ls /dev/cu.usb*You should see something like /dev/ttyACM0 (Linux), /dev/cu.usbmodem14101 (macOS), or COM3 (Windows).
You'll probably have to Press the reset button (#4) on the board to upload the firmware.
Linux (adjust port if different):
uv run pio run -t upload --upload-port /dev/ttyACM0macOS:
uv run pio run -t upload --upload-port /dev/cu.usbmodem14101You should see progress bars and finally:
========================= [SUCCESS] Took XX.XX seconds =========================
Go back to the project root directory:
cd ..Create the images directory and add some images:
mkdir -p images/default
cp your_photo.jpg images/default/Images will be automatically resized and converted to the display's 6-color palette. You can add multiple images and they will rotate on each refresh.
JPEG, PNG and HEIC are suppported.
Optional: add a schedule override file so frames only wake during the hours you care about:
cp device_config.example.json images/default/device_config.jsonThe same file can also live in images/<mac-address>/device_config.json for a specific board.
If you prefer not to edit JSON manually, open http://YOUR_SERVER_IP:5000/ after starting the server. The main page now includes embedded schedule editors for the global fallback, the default device schedule, and any devices that have already connected. The focused editor remains available at http://YOUR_SERVER_IP:5000/schedule.
uv run python image_server.pyYou should see:
Starting E-Ink Image Server (Multi-Device)...
PIL available: True
HEIC support: True
Default image: image.jpg
Images directory: /path/to/seeed_eink_board/images
Display size: 1600x1200
Device directories found: default, d0cf1326f7e8
* Serving Flask app 'image_server'
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.86.34:5000
Leave this running and open a new terminal for the next steps.
Open http://YOUR_SERVER_IP:5000/ in a browser. That page shows:
- connected devices
- battery voltage reported by each device
- the current image directory for each device
- embedded schedule editors for global, default, and per-device overrides
Press the reset button on the EE02 board.
The display should:
- Connect to WiFi (a few seconds)
- Sync current time and any schedule overrides from the server
- Download the image if needed
- Refresh the display (usually 20-30 seconds of flickering)
- Go to sleep
Do not worry if the first image takes a while. The server may spend extra time resizing and converting a large image before it starts sending the 960 KB packed display buffer.
Congratulations! (if that actually worked) Your e-ink display should be showing your image.
The ESP32 sends debug information over USB. Mainly helpful for troubleshooting.
screen /dev/ttyACM0 115200Press reset on the board to see output.
cd firmware
uv run pio device monitor --port /dev/ttyACM0 --baud 115200The USB serial device disappears when the board enters deep sleep, so a single pio device monitor session usually stops after the first sleep cycle.
This loop reattaches each time the board wakes up:
cd firmware
while true; do
uv run pio device monitor --port /dev/ttyACM0 --baud 115200
sleep 1
doneTo save that output to a file at the same time:
script -f /tmp/ee02-monitor.log -c 'bash -lc "cd /home/slzatz/seeed_eink_board/firmware; while true; do uv run pio device monitor --port /dev/ttyACM0 --baud 115200; sleep 1; done"'Normal operation looks like this:
========================================
Seeed EE02 E-Ink Display Firmware
========================================
Boot count: 1
========================================
NORMAL OPERATION MODE
========================================
Battery: ADC=2413, voltage=4.21V
Connecting to WiFi: YourNetwork
.
Connected! IP: 192....
Checking image hash at: http://192.168.86.34:5000/hash
Sending X-Device-MAC: d0cf1326f7e8
Last known hash: (none)
Server hash: 942d3cfc05c8fa41
Image changed - will download new image
Fetching image from: http://192.168.86.34:5000/image_packed
Content length: 960000 bytes
Downloaded 960000 bytes in 10395 ms
Spectra6: Starting display refresh...
Spectra6: Refresh complete in 28432 ms
WiFi disconnected
Entering deep sleep for 15 minutes...
Going to sleep now...
When the image hasn't changed:
Checking image hash at: http://192.168.86.34:5000/hash
Last known hash: 942d3cfc05c8fa41
Server hash: 942d3cfc05c8fa41
Image unchanged - skipping download
Image unchanged - going back to sleep
WiFi disconnected
Entering deep sleep for 15 minutes...
You can change the server address, sleep interval, and other settings without reflashing the firmware!
- Hold Button 1 (GPIO2 - the button closest to the USB connector)
- While holding Button 1, press and release the reset button (Button 4 next to on/off switch)
- Continue holding Button 1 for an additional second
- Release Button 1 - the device will enter configuration mode
The on-board firmware includes a simple web server for configuration when in config mode. It is generally easier to use the image server to make configuration changes but one reason to use the web interface to the EEO2 board is if you move the image server or change the port, you can enter the new values through this interface.
- Connect to your WiFi network
- The serial monitor will show the device's IP address
- Open a web browser and go to that IP address (e.g.,
http://192.168.86.24) - You'll see a configuration page where you can change:
- Server Host: The IP address of your image server
- Server Port: Usually 5000
- Image Endpoint: Usually
/image_packed - Refresh Interval: How often to check for new images during active hours (1-1440 minutes)
- Active Start / End Hour: Local wall-clock active window
- Timezone Offset: Minutes from UTC for local scheduling
- Click Save Configuration
- Click Reboot Device
If the device can't connect to your WiFi in config mode:
- It will create its own WiFi network called "EInk-Setup"
- Connect your phone or computer to "EInk-Setup"
- Open a browser to
http://192.168.4.1 - Configure the settings
You can run multiple EE02 boards from a single image server, each displaying different content. Each board is identified by its MAC address.
seeed_eink_board/
└── images/
├── default/ # Fallback for unknown devices
│ ├── image1.jpg
│ └── image2.png
├── d0cf1326f7e8/ # First board (MAC without separators)
│ ├── photo1.jpg
│ └── photo2.heic
└── aabbccddeeff/ # Second board
└── artwork.png
- Enter configuration mode (hold Button 1 during reset)
- Connect to the configuration page
- The Device Info section shows the MAC address and IP
- Use the MAC address (lowercase, no colons) as the directory name
- Each board sends its MAC address with every request via the
X-Device-MACheader - The server looks for
images/<mac-address>/directory - If not found, falls back to
images/default/ - Each board maintains its own rotation state independently
# Create directories
mkdir -p images/default
mkdir -p images/d0cf1326f7e8 # Kitchen display
mkdir -p images/a1b2c3d4e5f6 # Living room display
# Add images for each
cp kitchen_photos/*.jpg images/d0cf1326f7e8/
cp artwork/*.png images/a1b2c3d4e5f6/
cp fallback.jpg images/default/Each board will cycle through its own set of images independently.
The device isn't detected. Try:
- Different USB cable - This is the most common issue! Many cables are charge-only.
- Press the reset button - The device may be in deep sleep
- Check the port name - Run
ls /dev/ttyACM*(Linux) orls /dev/cu.usb*(macOS)
- Make sure your network is 2.4GHz
- Make sure you copied
config.h.exampletoconfig.h - Double-check the SSID and password in your
firmware/src/config.h - Rebuild and reflash after changing:
uv run pio run -t upload
The device can't reach the image server:
- Make sure the image server is running (
uv run python image_server.py) - Check that the server IP address is correct
- Make sure your firewall allows connections on port 5000
- Test from another device:
curl http://YOUR_SERVER_IP:5000/hash
- This display is inherently slow to refresh. A full refresh often takes 20-30 seconds.
- The server may also need extra time to resize and quantize a source image before it can send
/image_packed. - HEIC images are usually slower to process than JPEG or PNG.
- Watch the server terminal and the firmware log together if you need to separate server processing time from panel refresh time.
The display is designed for portrait orientation with the board at the bottom. If your image appears rotated, you can edit image_server.py and change the rotation value (line with img.rotate(270,)
seeed_eink_board/
├── README.md # This file
├── image_server.py # Python server that serves images to the display
├── image.jpg # Fallback image (optional)
├── images/ # Multi-device image directories
│ ├── default/ # Fallback for unknown devices
│ └── d0cf1326f7e8/ # Device-specific (MAC address)
├── firmware/ # ESP32 firmware
│ ├── platformio.ini # Build configuration
│ ├── README.md # Detailed firmware documentation
│ └── src/
│ ├── config.h.example # WiFi config template (copy to config.h)
│ ├── config_manager.h # Default server settings
│ └── ... # Other source files
└── pyproject.toml # Python project configuration
┌─────────────────┐ ┌─────────────────┐
│ Your Computer │ │ EE02 Board │
│ │ │ │
│ image_server.py│◄────────│ ESP32 Firmware │
│ :5000 │ WiFi │ │
│ │ │ │
│ image.jpg │ │ E-Ink Display │
└─────────────────┘ └─────────────────┘
1. ESP32 wakes from deep sleep
2. Reads battery voltage via on-board ADC
3. Connects to WiFi
4. Requests `/device_config` from server to sync time and optional schedule overrides
5. If local time is outside the active window: go back to deep sleep until the next start hour
6. Requests `/hash` from server (small request to check if image changed)
- Every server request includes `X-Device-MAC` and `X-Battery-Voltage` headers
7. If hash matches previous: go back to sleep (saves battery!)
8. If hash is different: download `/image_packed` (960KB)
9. Send data to e-ink display
10. Display refreshes
11. ESP32 enters deep sleep for the configured interval or until the next active window
12. Repeat from step 1
The firmware reads battery voltage on each wake cycle and sends it to the image server via the X-Battery-Voltage HTTP header on every request. The server records the latest value for device status and logs one battery line at the start of each wake cycle during /device_config. Server logs also include wall-clock timestamps plus the device MAC/IP prefix to make multi-device activity easier to follow.
| Voltage | Capacity | Status |
|---|---|---|
| 4.2V+ | Full (or on USB) | GOOD |
| 3.7V | ~50% | GOOD |
| 3.3V | ~10% | LOW |
| 3.0V | Empty (cutoff) | LOW |
Battery status is visible on the server index page and in the /current JSON endpoint.
- Firmware display driver inspired by esphome-bigink