Skip to content

Instantly share code, notes, and snippets.

@JoshData
Last active October 26, 2022 18:14
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save JoshData/9b1a987b373a6e9e4023db0c79743e64 to your computer and use it in GitHub Desktop.
Save JoshData/9b1a987b373a6e9e4023db0c79743e64 to your computer and use it in GitHub Desktop.
Waveshare e-paper driver tool
/*
* 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;
}
@stan69b
Copy link

stan69b commented Oct 23, 2022

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

@JoshData
Copy link
Author

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