Standalone E-Ink Picture Frame

hardware e-ink tooling

I came across this blog post by Guy Sie detailing a Spectra 6 based e-ink display to use as a picture frame. The premise is quite simple and enticing: a dynamic picture frame that doesn’t look like a display and can show images relatively well as long as the images are encoded properly for it. I ended up getting one and set it up to work with Home Assistant.

Disclaimer: The Arduino firmware for this project and parts of Ink Frame Lab has been developed with help from AI tools. The design decisions, architecture, and hardware debugging were done manually, with AI assisting primarily in code generation and iteration.

Its nice, but..

After living with the Home Assistant setup for a while, a few friction points became clear:

Ideas and dead ends

My first idea was to expose the SD card as a USB drive when the device is plugged in, so you could just drag and drop images like a thumb drive. This turned out to be a hardware dead end: on the reTerminal E1002, the USB-C port is routed through a CH341 UART bridge chip, which can only do serial communication. The ESP32-S3 does have native USB OTG that could theoretically do mass storage, but those GPIO pins (19/20) are repurposed for the I2C bus on this board. There’s no way to present a storage device to the host without physically modifying the PCB.

The fallback was Wi-Fi. The ESP32-S3 has Wi-Fi built in, so the device could host a small web server with a drag-and-drop upload page. No app needed, works from any phone or laptop browser. The question was how to make this accessible to someone who has never configured a microcontroller. The answer turned out to be using the device’s own Wi-Fi access point - the frame creates its own network, the e-ink screen shows the network name, password, and URL in large text, and you just follow the steps. No router configuration, no IP address hunting.

The solution

I ended up building two things: custom Arduino firmware for the reTerminal E1002, and Ink Frame Lab — a browser-based tool for preparing images for e-ink displays.

Firmware for reTerminal E1002

The firmware replaces the stock ESPHome setup with standalone Arduino code that doesn’t need Wi-Fi or Home Assistant during normal operation. The device reads PNG images from the SD card, picks one (randomly or sequentially based on a config file), renders it to the e-ink display with a single-pixel horizontal battery indicator bar at the bottom, and goes into deep sleep until it’s time to change.

The interesting engineering challenges were all around the shared SPI bus. The SD card and e-ink display share the same SPI pins (MOSI, MISO, SCK) with separate chip selects, which means they can’t talk at the same time. My first approach was to decode the PNG and draw to the display simultaneously inside GxEPD2’s paged drawing loop - re-reading the PNG from SD for each page. This worked for the first 40-pixel strip and then the rest of the screen was white. The display’s SPI context was active during the page loop, so the SD card reads silently failed.

The fix was a two-pass approach using the ESP32-S3’s 8MB PSRAM: first, decode the entire PNG from SD into a 384KB buffer in PSRAM, close the SD card, then initialize the display and draw from the buffer. This also meant being deliberate about initialization order - if the display driver sent its init sequence on the shared SPI bus before the SD card was mounted, the card’s internal SPI state machine would get confused and reject subsequent mount attempts. Splitting initDisplay() into a pin-setup phase and a deferred driver-init phase fixed this, ensuring the SD card always gets a clean bus.

Another entertaining bug: the first successful render had all the colors wrong. Green foliage showed as red, blue sky showed as green. The PNGdec library’s getLineAsRGB565() function was being called with PNG_RGB565_BIG_ENDIAN, which byte-swaps each pixel for big-endian displays - but the ESP32 is little-endian. The bit extraction was pulling the wrong channels from each swapped uint16_t. A one line fix to PNG_RGB565_LITTLE_ENDIAN and the colors were correct.

The web server mode is a secondary boot mode activated by holding the green button during power-on. The e-ink screen shows step-by-step instructions with the Wi-Fi credentials and URL, and the web interface lets you upload, delete, and manage photos, configure the rotation interval, and choose between random or sequential display order. In sequential mode, the two white buttons on the device navigate forward and backward through the images. When you’re done, the device enters deep sleep for a couple of seconds and wakes up as a clean cold boot into slideshow mode - I learned the hard way that ESP.restart() is a software reset that doesn’t properly reinitialize the SPI peripheral, so the SD card would fail to mount after every restart from setup mode.

As a side effect of running standalone and not needing to maintain a Wi-Fi connection, battery life improved significantly. My device is set to change images randomly every 4 hours and loses about 10% over a week.

Here are the firmware files for reTerminal E1002 and installation instructions.

Ink Frame Lab

The image preparation side of the problem needed its own tool. Existing dithering tools handle the palette conversion, but none of them solve the full workflow: crop to the panel’s aspect ratio, resize to 800×480, dither to the Spectra 6 palette, and preview how it will actually look on the muted, non-backlit display.

Ink Frame Lab is a browser-based tool that handles all of this. You import images, crop them with a locked aspect ratio, preview the dithered result, and - the part I’m most pleased with - inspect it in a 3D view that simulates different lighting conditions and angles. This matters more than you’d think: an image that looks great on your monitor can look muddy or washed out on the actual panel, and being able to preview that before exporting saves a lot of trial and error. Oh, and you can bulk edit and export images - no need to process images one at a time.

You can read more about this tool here and a functional web version is here.

Credits

Future plans