Frame by frame
An e-paper digital picture frame
I got an e-paper display and a picture frame as a gift, so naturally, we're building a home cluster out of old mobile phones. Just kidding. We're building a digital photo frame. If you were more into the cluster idea, too bad you can check out this article.
On (e-)paper, the whole thing is simple. An ESP32 is hooked up to the display, hanging out on the network, waiting for new images to refresh the screen. But we also need to generate those images somehow, because the display only supports black and white.
Assembly
The passe-partout (a new word I learned during this project; before that, it was known as "that thing inside the frame around the picture") that came with the frame didn't quite match the size of the e-paper display. I could've found a piece of cardboard with the right thickness, but instead, I fired up FreeCAD, designed a new one, and 3D printed it.
The display fits perfectly (after 2-3 test prints), and there's even a bit of room for the ribbon cable.
Sharp-eyed readers might even be able to tell which xkcd is currently on the display.
I had to cut a small hole in the back of the frame so the cable could pass through and connect to the ESP32.
You have to be careful with the ribbon cable, it's pretty fragile. Don't ask me how I know. And of course, I'm not using a cable this short because I somehow messed up the connector on the extension. Definitely not.
In the end, I attached the ESP32 in the least aesthetic way possible.
I tried powering the whole thing from an IKEA power bank, but it turns out those things shut themselves off if there's not enough power drawn. And the e-paper display apparently doesn't use enough juice to keep it awake.
Software
First things first, let's try displaying something to make sure everything works. I set up the ESP32 with the Arduino IDE using the manufacturer's documentation, and then, again following the docs, displayed an image.
#include <DEV_Config.h>
#include <EPD.h>
#include <GUI_Paint.h>
UBYTE *image;
UWORD imageSize = ((EPD_7IN5_V2_WIDTH % 8 == 0) ? (EPD_7IN5_V2_WIDTH / 8) : (EPD_7IN5_V2_WIDTH / 8 + 1)) * EPD_7IN5_V2_HEIGHT;
uint8_t *imageData = nullptr;
void setup() {
DEV_Module_Init();
EPD_7IN5_V2_Init();
EPD_7IN5_V2_Clear();
DEV_Delay_ms(500);
if ((image = (UBYTE *)malloc(imageSize)) == NULL) {
Serial.println("Failed to apply for image memory");
while (1);
}
Paint_NewImage(image, EPD_7IN5_V2_WIDTH, EPD_7IN5_V2_HEIGHT, 0, WHITE);
Paint_SelectImage(image);
Paint_Clear(WHITE);
Paint_DrawBitMap(imageData);
EPD_7IN5_V2_Display(image);
DEV_Delay_ms(2000);
}
The imageData has to come from somewhere. You could hardcode some byte arrays into your program, but that's not very flexible. So it's time to think about how we actually want to update the image on the display.
Since we have Wi-Fi, we can go one of two routes: either act as a server or as a client. I went with the former, found a web server library that works with the hardware and created an /upload endpoint.
// ...
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
// ...
static AsyncWebServer server(80);
bool imageChanged = false;
void on_request(AsyncWebServerRequest *request) {
if (request->getResponse()) {
return;
}
if (!request->_tempObject) {
return request->send(400, "text/plain", "Nothing uploaded");
}
imageData = reinterpret_cast<uint8_t *>(request->_tempObject);
imageChanged = true;
request->_tempObject = nullptr;
request->send(200, "text/plain", "OK");
}
void on_upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
Serial.printf("Upload[%s]: start=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);
if (!index) {
size_t size = request->header("Content-Length").toInt();
if (!size) {
request->send(400, "text/plain", "No Content-Length");
} else {
Serial.printf("Allocating buffer of %u bytes\n", size);
uint8_t *buffer = new (std::nothrow) uint8_t[size];
if (!buffer) {
request->abort();
} else {
request->_tempObject = buffer;
}
}
}
if (len) {
memcpy(reinterpret_cast<uint8_t *>(request->_tempObject) + index, data, len);
}
}
void update_image() {
Paint_SelectImage(image);
Paint_Clear(WHITE);
Paint_DrawBitMap(imageData);
EPD_7IN5_V2_Display(image);
DEV_Delay_ms(2000);
delete imageData;
imageData = nullptr;
imageChanged = false;
}
void setup() {
// ...
server.on("/upload", HTTP_POST, on_request, on_upload);
server.begin();
}
void loop() {
if (imageChanged) {
update_image();
}
delay(1000);
}
Now we can update the image with a simple curl command:
$ curl -v -F "data=@something.wbm" "http://192.168.255.50/upload"
Assuming we have the image in the right format, of course. This display only understands black and white (not even grayscale) so we'll need a bit of trickery.
Image Generation
First, grab the image you want to display, sized at 800x480 pixels (the resolution of the display). Open it in Krita, and go to Settings > Dockers > Palette to bring up the palettes. Click the icon in the lower left corner to open the palette chooser, hit the + to add a new one, name it something like "1bit", and set black and white as the only colors.
You can close the palette windows after this, we won't need them anymore. Now go to Filter > Map > Palettize..., select your "1bit" palette, check the Dither box, and pick a pattern from the list that makes your image look halfway decent.
It's also worth playing around with the Color Mode and Offset Scale settings.
Just like before, WBMP would be a perfect format for us, but Krita doesn't support it, so we'll save the image as a PNG. Then, we'll use ImageMagick's convert command to turn it into a WBMP file:
$ convert image.png image.wbmp
The resulting file will have a 6-byte WBMP header at the start, which we don't need, so let's get rid of it.
$ tail -c +7 image.wbmp >image_no_header.wbmp
Now we can upload the file just like we did earlier:
$ curl -v -F "data=@image_no_header.wbmp" "http://192.168.255.50/upload"
And finally, a close-up of those sweet e-paper pixels:
This project can be extended in all kinds of directions. For example, with a program that converts any image to the proper format and uploads it to the frame. There are even more advanced e-paper displays out there now, with grayscale and even color support. But for now, we've reached the end of this journey, and time to move out of frame.