Last active
November 10, 2024 13:21
-
-
Save JoshData/9b1a987b373a6e9e4023db0c79743e64 to your computer and use it in GitHub Desktop.
Waveshare e-paper driver tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* A Waveshare E-Paper controller via a simple web API | |
* | |
* This C++ program creates a simple HTTP server to control a Waveshare | |
* e-Paper display, like the 10.3inch e-Paper e-Ink Display HAT For | |
* Raspberry Pi at https://www.waveshare.com/10.3inch-e-Paper-HAT.htm | |
* which I am using. This program is meant to be run on the Pi that is | |
* connected to the e-Paper display. | |
* | |
* When run, this program creates a simple web server that serves a | |
* form on which you can submit text, HTML, or an image file, which | |
* will be displayed on the e-Paper display. Images will automatically | |
* be converted to dithered 4-bit greyscale and cropped and scaled to | |
* fit the display using imagemagick. HTML will be automatically rendered | |
* to an image using wkhtmltoimage, with the font size auto-selected so | |
* that the page fits (and covers) the display. Text will be interpreted | |
* as CommonMark and converted to HTML using pandoc, and then converted | |
* to an image the same as HTML submitted directly. | |
* | |
* This uses and is based on the sample C library provided by Waveshare | |
* at https://www.waveshare.com/wiki/10.3inch_e-Paper_HAT. | |
* | |
* NOTE: Every e-Paper display comes with a tiny label that gives a "VCOM" | |
* number. Multiply it by 1000, remove the negative sign, and set it in | |
* the VCOM constant below. I think this has to do with image contrast | |
* but I'm not sure, so I don't know what kind of damage you can cause if | |
* you don't do this. | |
* | |
* Prerequisites: | |
* | |
* Following the instructions at https://www.waveshare.com/wiki/10.3inch_e-Paper_HAT, | |
* install the bcm2835 driver library which controls the GPIO headers that | |
* the display is connected to: | |
* wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.60.tar.gz | |
* tar zxvf bcm2835-1.60.tar.gz | |
* cd bcm2835-1.60/ | |
* ./configure | |
* make | |
* sudo make check | |
* sudo make install | |
* Enable the SPI interface by running | |
* sudo raspi-config | |
* and choosing Interface Option -> SPI -> Yes. | |
* And download the Waveshare example application: | |
* git clone https://github.com/waveshare/IT8951-ePaper.git | |
* cd IT8951-ePaper | |
* make | |
* Test the dispay: | |
* sudo ./epd {VCOM} 1 # NOTE: Replace {VCOM} with the VCOM value as shown on | |
* # the device, like -1.50. | |
* | |
* Then get the tools used by this application: | |
* sudo apt install imagemagick wkhtmltopdf pandoc | |
* wget https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h | |
* | |
* And place this file in the IT8951-ePaper directory. | |
* | |
* Build: | |
* | |
* To build, make sure you've built the Waveshare IT8951-ePaper sample first, | |
* as above. Then compile this file: | |
* | |
* g++ -o ../epapercmd epapercmd.cpp ./bin/{font24,GUI_BMPfile,GUI_Paint,DEV_Config,EPD_IT8951}.o -lbcm2835 -lm -lpthread -g -O0 -Wall | |
* | |
* Run: | |
* | |
* Start this application as root and give it the network interface (e.g., 127.0.0.1 | |
* or 0.0.0.0) to bind to, plus a port number to listen on: | |
* | |
* sudo ../epapercmd 0.0.0.0 8000 | |
* | |
* (Add '/home/pi/epapercmd 0.0.0.0 8000 &' to /etc/rc.local to start on boot in the background.) | |
* | |
* Visit http://{your pi's IP address}:8000 to see the form to submit something to | |
* display on the device. Or call the API directly: | |
* | |
* Show an image on the display (resizing to fit the display): | |
* curl -Fimage=@/path/to/image.jpeg http://{your pi's IP address}:8000/image | |
* | |
* Show an image at its native resolution at a particular location on the display (x=left, y=top): | |
* curl -Fimage=@/tmp/capitol.jpeg -Fx=50 -Fy=50 http://{your pi's IP address}:8000/image | |
* | |
* Show some HTML: | |
* curl -Fhtml="<b>Hello</b>" http://{your pi's IP address}:8000/html | |
* | |
* Show some text: | |
* curl -text="<b>Hello</b>" http://{your pi's IP address}:8000/text | |
*/ | |
const UWORD VCOM = 1250; | |
#include <stdio.h> | |
#include <memory.h> | |
#include <iostream> | |
extern "C" { | |
#include "lib/Config/DEV_Config.h" | |
#include "lib/e-Paper/EPD_IT8951.h" | |
#include "lib/GUI/GUI_BMPfile.h" | |
#include "lib/GUI/GUI_Paint.h" | |
#include "lib/GUI/GUI_BMPfile.h" | |
} | |
#include "httplib.h" | |
struct panel_info { | |
UWORD Width; | |
UWORD Height; | |
UDOUBLE Addr; | |
char BitsPerPixel; | |
UBYTE* ImageBuffer; | |
}; | |
char *shell(const char* cmd) { | |
// Executes the command and returns the output from its STDOUT. | |
// The commands should be trusted --- don't put any user-supplied | |
// content in the command, or risk creating a security vulnerability. | |
printf("<%s> ...\n", cmd); | |
char buffer[512]; | |
char* output = (char*)malloc(512); | |
output[0] = 0; | |
FILE* stream = popen(cmd, "r"); | |
if (stream) { | |
while (!feof(stream)) | |
if (fgets(buffer, sizeof(buffer), stream) != NULL) | |
strcat(output, buffer); | |
pclose(stream); | |
} | |
while (output[strlen(output)-1] == '\n' || output[strlen(output)-1] == '\r') | |
output[strlen(output)-1] = 0; | |
return output; | |
} | |
void refresh(const panel_info& panel_info) { | |
switch (panel_info.BitsPerPixel) { | |
case 8: | |
EPD_IT8951_8bp_Refresh(panel_info.ImageBuffer, 0, 0, panel_info.Width, panel_info.Height, false, panel_info.Addr); | |
break; | |
case 4: | |
EPD_IT8951_4bp_Refresh(panel_info.ImageBuffer, 0, 0, panel_info.Width, panel_info.Height, false, panel_info.Addr, false); | |
break; | |
case 2: | |
EPD_IT8951_2bp_Refresh(panel_info.ImageBuffer, 0, 0, panel_info.Width, panel_info.Height, false, panel_info.Addr, false); | |
break; | |
case 1: | |
EPD_IT8951_1bp_Refresh(panel_info.ImageBuffer, 0, 0, panel_info.Width, panel_info.Height, A2_Mode, panel_info.Addr, false); | |
break; | |
} | |
} | |
void show_image(const char* filename, bool fill, int x, int y, const panel_info& panel_info) { | |
// The GUI_ReadBmp function only supports BMP images, so we'll use the ImageMagick 'convert' tool to | |
// convert whatever we get to that format. Additionally, we want greater control over the conversion | |
// to the greyscale & bit depth supported by the panel. '-greyscale average -depth {BitsPerPixel}' | |
// will do it, but it doesn't do dithering. To reach the right bit depth with nice dithering, we need to | |
// use '-colors {2^BitsPerPixel}' and we can combine this with '-quantize gray' to get to greyscale in | |
// the same step. However, images made with -colors breaks GUI_ReadBmp unless we output in BMP format 3, | |
// rather than the default format version 4. '-depth {BitsPerPixel}' may be superfluous after this but | |
// might save some bytes in output. | |
// | |
// Also crop (centered) and scale the image to the right size to fit the screen, unless 'x' and 'y' | |
// parameters are given, in which case keep the native pixel size and just update the screen in the | |
// rectangle (x, y)-(x+width,y+height). | |
char convert_cmd[1024] = "convert"; | |
char buf[1024] = ""; | |
if (fill) { | |
sprintf(buf, " -gravity center -crop %d:%d -resize %dx%d", panel_info.Width, panel_info.Height, panel_info.Width, panel_info.Height); | |
strcat(convert_cmd, buf); | |
} | |
sprintf(buf, " -quantize gray -colors %d -depth %d", 1<<panel_info.BitsPerPixel, panel_info.BitsPerPixel); | |
strcat(convert_cmd, buf); | |
sprintf(buf, " %s bmp3:/tmp/image.bmp", filename); | |
strcat(convert_cmd, buf); | |
shell(convert_cmd); | |
// Load the image into the display buffer. | |
GUI_ReadBmp("/tmp/image.bmp", x, y); | |
//Paint_DrawRectangle(50, 50, Panel_Width/2, Panel_Height/2, 0x30, DOT_PIXEL_3X3, DRAW_FILL_EMPTY); | |
//Paint_DrawCircle(Panel_Width*3/4, Panel_Height/4, 100, 0xF0, DOT_PIXEL_2X2, DRAW_FILL_EMPTY); | |
//Paint_DrawNum(Panel_Width/4, Panel_Height/5, 709, &Font20, 0x30, 0xB0); | |
//Paint_DrawString_EN(10, 10, "8 bits per pixel 16 grayscale", &Font24, 0xF0, 0x00); | |
refresh(panel_info); | |
} | |
void show_html(const char* htmlfn, int x, int y, int width, int height, panel_info panel_info) { | |
char buf[1024]; | |
sprintf(buf, "wkhtmltoimage -q -f png --width %d --height %d %s /tmp/image.png", | |
width, height, htmlfn); | |
shell(buf); | |
show_image("/tmp/image.png", false, x, y, panel_info); | |
} | |
void show_html_autosize(const char* htmlfn, int x, int y, int width, int height, panel_info panel_info) { | |
// Choose a font size by an iterative process that finds the maximum font size | |
// that stays within the width and height. | |
// Read the HTML from the file. (https://stackoverflow.com/a/116177) | |
std::ifstream ifs(htmlfn); | |
std::string html(std::istreambuf_iterator<char>{ifs}, {}); | |
// Iterate between 1vw and 50vw. | |
float min_size = 1, max_size = 50; | |
float size = 10; // initial size | |
int iters = 0; | |
while (iters++ < 10) { | |
// Write the html to a file but add a <style> block at the start. | |
FILE* f = fopen("/tmp/content.html", "w"); | |
fprintf(f, "<style>body { font-size: %gvw }</style>\n", round(size*10)/10); | |
fputs(html.c_str(), f); | |
fclose(f); | |
// Render. If any words are too wide for the width, wkhtml returns an image larger | |
// than the requested width. So we can test for words that would be cropped by | |
// checking if the returned image width is wider than the desired width. | |
char cmd[1024]; | |
sprintf(cmd, "wkhtmltoimage -q --width %d --crop-w %d --crop-h %d -f png /tmp/content.html /tmp/image.png", | |
width, width*2, height*2); | |
shell(cmd); | |
// Get image dimensions. | |
int im_w = atoi(shell("identify -format '%w' /tmp/image.png")); | |
int im_h = atoi(shell("identify -format '%h' /tmp/image.png")); | |
printf("size=%g => %d,%d\n", size, im_w, im_h); | |
if (im_h > height || im_w > width) { | |
// Make smaller. | |
max_size = size; | |
size = (size + min_size) / 2; | |
} else if (size <= (min_size+.5) || (size >= max_size-.5)) { | |
// Converged on a font size. Whatever remains in the HTML is good. | |
break; | |
} else { | |
// Make bigger. | |
min_size = size; | |
size = (size + max_size) / 2; | |
} | |
} | |
show_html("/tmp/content.html", x, y, width, height, panel_info); | |
} | |
// Helper function to get a parameter either from multipart/form-data (which is more convenient | |
// for submitting file content using curl) or application/x-www-form-urlencoded (which is | |
// more convenient in most client libraries). | |
void get_param(const char* name, const httplib::Request& req, std::function<void(const std::string&)> getter) { | |
if (req.has_file(name)) | |
getter(req.get_file_value(name).content); | |
else if (req.has_param(name)) | |
getter(req.get_param_value(name)); | |
} | |
int main(int argc, char** argv) { | |
if (argc < 3) { | |
fprintf(stderr, "Usage: ./epapercmd 0.0.0.0 8000\n"); | |
return 1; | |
} | |
// Get my IP address to be able to display it on startup. | |
char* network_ip = shell("hostname -I"); | |
// Panel properties. | |
panel_info panel_info; | |
panel_info.BitsPerPixel = 4; | |
// Initialize display. | |
if (DEV_Module_Init() != 0) return 2; | |
IT8951_Dev_Info Dev_Info = EPD_IT8951_Init(VCOM); | |
panel_info.Width = Dev_Info.Panel_W; | |
panel_info.Height = Dev_Info.Panel_H; | |
panel_info.Addr = Dev_Info.Memory_Addr_L | (Dev_Info.Memory_Addr_H << 16); | |
EPD_IT8951_Init_Refresh(Dev_Info, panel_info.Addr); | |
// Initialize buffer. | |
UDOUBLE image_buffer_size = ((panel_info.Width * panel_info.BitsPerPixel % 8 == 0)? (panel_info.Width * panel_info.BitsPerPixel / 8 ): (panel_info.Width * panel_info.BitsPerPixel / 8 + 1)) * panel_info.Height; | |
panel_info.ImageBuffer = (UBYTE *)malloc(image_buffer_size); | |
Paint_NewImage(panel_info.ImageBuffer, panel_info.Width, panel_info.Height, 0, BLACK); | |
Paint_SelectImage(panel_info.ImageBuffer); | |
Paint_SetBitsPerPixel(panel_info.BitsPerPixel); | |
Paint_Clear(WHITE); | |
Paint_SetMirroring(MIRROR_HORIZONTAL); | |
// Show the IP address until we get the first command. | |
Paint_DrawString_EN(10, 10, network_ip, &Font24, BLACK, WHITE); | |
refresh(panel_info); | |
// Read commands. | |
printf("Waiting for commands...\n"); | |
httplib::Server svr; | |
svr.Get("/", [&](const auto& req, auto& res) { | |
// Show a form to submit something to post. | |
std::stringstream s; | |
s << "<h5>Text</h5>" << std::endl; | |
s << "<form action=text method=post>" << std::endl; | |
s << "<input name=text> <input type=submit value=Post>" << std::endl; | |
s << "</form>" << std::endl; | |
s << "<h5>HTML</h5>" << std::endl; | |
s << "<form action=html method=post>" << std::endl; | |
s << "<textarea name=html></textarea>" << std::endl; | |
s << "<input type=submit value=Post>" << std::endl; | |
s << "</form>" << std::endl; | |
s << "<h5>Image</h5>" << std::endl; | |
s << "<form action=image method=post enctype=multipart/form-data>" << std::endl; | |
s << "<input type=file name=image>" << std::endl; | |
s << "<input type=submit value=Post>" << std::endl; | |
s << "</form>" << std::endl; | |
res.set_content(s.str().c_str(), "text/html"); | |
}); | |
svr.Post("/image", [&](const auto& req, auto& res) { | |
bool fill = true; | |
int x = 0, y = 0; | |
bool found_file = false; | |
// Copy the 'file' attachment to a file on disk and read the 'x' and 'y' parameters. | |
get_param("image", req, [&](auto value) { | |
FILE* bmp = fopen("/tmp/image.in", "w"); | |
fwrite(value.c_str(), 1, value.size(), bmp); | |
fclose(bmp); | |
found_file = true; | |
}); | |
get_param("x", req, [&](auto value) { | |
x = atoi(value.c_str()); | |
fill = false; | |
}); | |
get_param("y", req, [&](auto value) { | |
y = atoi(value.c_str()); | |
fill = false; | |
}); | |
if (!found_file) { | |
res.status = 400; | |
res.set_content("No 'image' given.", "text/plain"); | |
return; | |
} | |
show_image("/tmp/image.in", fill, x, y, panel_info); | |
res.set_content("OK", "text/plain"); | |
}); | |
svr.Post("/html", [&](const auto& req, auto& res) { | |
// Default placement fills the panel. | |
std::string html; | |
int x = 0, y = 0, width = panel_info.Width, height = panel_info.Height; | |
bool auto_size_font = true; | |
bool found_content = false; | |
// Get the parameters. | |
get_param("html", req, [&](auto value) { html = value; found_content = true; }); | |
get_param("x", req, [&](auto value) { x = atoi(value.c_str()); }); | |
get_param("y", req, [&](auto value) { y = atoi(value.c_str()); }); | |
get_param("width", req, [&](auto value) { width = atoi(value.c_str()); }); | |
get_param("height", req, [&](auto value) { height = atoi(value.c_str()); }); | |
if (!found_content) { | |
res.status = 400; | |
res.set_content("No 'html' given.", "text/plain"); | |
return; | |
} | |
// Write the content to a file to render. | |
FILE* f = fopen("/tmp/content.html", "w"); | |
fwrite(html.c_str(), 1, html.size(), f); | |
fclose(f); | |
if (auto_size_font) { | |
show_html_autosize("/tmp/content.html", x, y, width, height, panel_info); | |
} else { | |
// Render. | |
show_html("/tmp/content.html", x, y, width, height, panel_info); | |
} | |
res.set_content("OK", "text/plain"); | |
}); | |
svr.Post("/text", [&](const auto& req, auto& res) { | |
// Default placement fills the panel. | |
std::string text; | |
int x = 0, y = 0, width = panel_info.Width, height = panel_info.Height; | |
bool auto_size_font = true; | |
bool found_content = false; | |
// Get the parameters. | |
get_param("text", req, [&](auto value) { text = value; found_content = true; }); | |
get_param("x", req, [&](auto value) { x = atoi(value.c_str()); }); | |
get_param("y", req, [&](auto value) { y = atoi(value.c_str()); }); | |
get_param("width", req, [&](auto value) { width = atoi(value.c_str()); }); | |
get_param("height", req, [&](auto value) { height = atoi(value.c_str()); }); | |
if (!found_content) { | |
res.status = 400; | |
res.set_content("No 'text' given.", "text/plain"); | |
return; | |
} | |
// Write to file to pass to pandoc. | |
FILE* f = fopen("/tmp/content.txt", "w"); | |
fputs(text.c_str(), f); | |
fclose(f); | |
// Convert text to HTML interpreting it as commonmark. | |
shell("pandoc -f commonmark -t html /tmp/content.txt -o /tmp/content.html"); | |
if (auto_size_font) { | |
// Read it back. | |
show_html_autosize("/tmp/content.html", x, y, width, height, panel_info); | |
} else { | |
// Render. | |
show_html("/tmp/content.html", x, y, width, height, panel_info); | |
} | |
res.set_content("OK", "text/plain"); | |
}); | |
svr.listen(argv[1], atoi(argv[2])); | |
// Terminate. | |
printf("Exiting.\n"); | |
EPD_IT8951_Sleep(); | |
//In case RPI is transmitting image in no hold mode, which requires at most 10s | |
//DEV_Delay_ms(5000); | |
DEV_Module_Exit(); | |
return 0; | |
} | |
Thanks for posting your experience! Next time I hack on this I'll look at your changes. I'm glad it seems to be helpful as a starting point.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for this script and the instructions :)
It does not appear to work anymore though so I forked it and fixed some issues.
UWORD constant was not defined
uint16_t did not exist because of missing import
Clear function not existing enymore and has been replaced + added new needed argument
Only images work though, the font lib seem to not like the UTF8 format, aying it is not a valid region tag..... I needed it for images so i did not look into it. available at : https://gist.githubusercontent.com/stan69b/673ccedd45b016f6a0b17e415af9a203/raw/6bfaad55264cf721607871cc38744a77f74edb83/epapercmd.cpp