Turn an ESP8266 into an IoT display

Table of Contents
The Idea
Iโve had this display lying around for a while, and I already created a post that shows that I managed to get it to work:
I uploaded the code to GitHub, that demonstrates how to get this display to work with an ESP8266/D1mini: arduino-esp8266-ILI9486. This is exactly the code I used in the post above.
The next step was to show something on this screen that goes beyond a static two colored (black and white) bitmap encoded into a header file, and something that google did not show up when I searched for it: A display with an API
I wanted to give this display an api, to upload images to it, initially via:
curl -i -X POST -F "data=@image.rgb" "http://192.168.178.50/api/rgb?x=0&y=0&w=300&h=480"
The API takes the parameters
x
the start point to draw on the x axisy
the start point to draw on the y axisw
the width of the uploaded imageh
the height of the uploaded image
The last two parameters w
and h
are the important ones as we are going to upload raw bitmap data. You can obtain this data or file by converting any image with
convert image.jpg image.rgb
I assume you have Imagemagick installed, and you made sure itโs scaled appropriately to 480x320 pixels ;).
The *.rgb
file is just a sequence of bytes, three colors (R,G,B) for each pixel, without any further meta data as in a .bmp
. That made the parsing and sending the data to the display easier, as we do not have to understand any codecs or fileformats for this on the ESP8266.
The Result
The Code
Hint: To run the code you need to create a file called config.h
that contains:
#define CFG_WIFI_SSID "YourWIFI"
#define CFG_WIFI_PASS "S3cr3t"
FYI: Iโm not a fan of blogposts that only show snippets of code, so here is
- the source file as a direct download: iot-screen.ino
- and the source again, embedded in this post, if you just want to browse:
#include "config.h"
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <SPI.h>
#include <Ucglib.h>
ESP8266WebServer server(80);
Ucglib_ILI9486_18x320x480_HWSPI ucg(/*cd=*/15, /*cs=*/0, /*reset=*/16);
uint8_t fontHeight = 0;
// upload params
uint16_t x = 0;
uint16_t y = 0;
uint16_t w = 300;
uint16_t h = 480;
// current x/y
uint16_t cx = 0;
uint16_t cy = 0;
uint8_t incompletePixel[3];
uint8_t incompleteSize = 0;
bool hasImage = false;
void connectWifi() {
WiFi.begin(CFG_WIFI_SSID, CFG_WIFI_PASS);
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
}
void setupDisplay() {
ucg.begin(UCG_FONT_MODE_TRANSPARENT);
ucg.setFont(ucg_font_courB14_mf);
fontHeight = 20;
ucg.clearScreen();
}
void parseUploadParams() {
// TODO: error handling?
String v = server.arg("x");
x = v.toInt();
v = server.arg("y");
y = v.toInt();
v = server.arg("w");
w = v.toInt();
v = server.arg("h");
h = v.toInt();
// reset current draw location
cx = 0;
cy = 0;
incompleteSize = 0;
}
void rgbApi() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.print("handleFileUpload Name: ");
Serial.println(upload.filename);
hasImage = true;
} else if (upload.status == UPLOAD_FILE_WRITE) {
// Problem: buf is 2048 bytes, which means it contains 682 pixels.
// 682*3 = 2046bytes, and the last pixel is not complete
// We need to store incomplete pixel data.
// we have an overlapping pixel
uint16_t loopStart = 0;
uint16_t loopEnd = 0;
if (incompleteSize) {
if (incompleteSize == 1) {
ucg.setColor(0, incompletePixel[0], upload.buf[0], upload.buf[1]);
loopStart = 2;
} else if (incompleteSize == 2) { // -> loop starts at 1
ucg.setColor(0, incompletePixel[0], incompletePixel[1], upload.buf[0]);
//ucg.setColor(0, 0, 0, 255);
loopStart = 1;
}
ucg.drawPixel(x + cx, y + cy);
cx++;
if (cx >= w) {
// move to next line
cx = 0;
cy++;
}
}
for (uint16_t i = loopStart; i < upload.currentSize - 3; i += 3) {
ucg.setColor(0, upload.buf[i], upload.buf[i + 1], upload.buf[i + 2]);
ucg.drawPixel(x + cx, y + cy);
cx++;
if (cx >= w) {
// move to next line
cx = 0;
cy++;
}
loopEnd = i + 3;
}
if (loopEnd < upload.currentSize ) {
// we have incomplete pixel data at the end of
// the buffer -> store it!
incompleteSize = upload.currentSize - loopEnd;
for (uint8_t i = 0; i < incompleteSize; i++) {
incompletePixel[i] = upload.buf[loopEnd + i];
}
}
} else if (upload.status == UPLOAD_FILE_END) {
Serial.print("Received Total: ");
Serial.println(upload.totalSize);
server.send(200);
} else {
server.send(500, "text/plain", "500: error");
}
}
void setupServer() {
server.on("/api/rgb", HTTP_POST, []() {
parseUploadParams();
server.send(200);
}, rgbApi);
server.begin();
}
void setup(void) {
Serial.begin(9600);
delay(1000);
setupDisplay();
WiFi.hostname("iotdisplay");
setupServer();
}
void loop(void) {
while (WiFi.status() != WL_CONNECTED) {
hasImage = false;
delay(500);
ucg.setColor(255, 255, 255);
ucg.setPrintPos(0, 1 * fontHeight);
ucg.print("Connecting...");
}
if (!hasImage) {
ucg.setColor(0, 255, 0);
ucg.setPrintPos(0, 1 * fontHeight);
ucg.print("Connected, IP address: ");
ucg.setPrintPos(0, 2 * fontHeight);
ucg.print(WiFi.localIP());
}
delay(50);
server.handleClient();
}
Live Desktop Feed
On macOs you can run the following bash script to send your current desktop to the tiny screen:
# example
bash screen-grabber.sh 192.168.178.50
screen-grabber.sh:
#!/bin/bash
SCREENHOST="$1"
while (true);
do
screencapture screen.png
convert screen.png -resize 480x300 -rotate 90 screen.rgb
curl -i -X POST -F "data=@screen.rgb" "http://$SCREENHOST/api/rgb?x=0&y=0&w=300&h=480"
done
Next Steps
Next steps would be to get a second D1mini and attach a camera to it, that sits infront of my 3D printer, so I can use this setup as a tiny mobile display to remotely monitor my prints :).