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" ""

The API takes the parameters

  • x the start point to draw on the x axis
  • y the start point to draw on the y axis
  • w the width of the uploaded image
  • h 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

When the microcontroller boots, it connets to the configured WiFi and displays the IP. You will need the IP later.
Then you can use the API to upload raw bitmap images...
... or a script that automatically uploads screenshots from your computer.

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

  1. the source file as a direct download: iot-screen.ino
  2. 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() {
  Serial.print("Connected, IP address: ");

void setupDisplay() {
  fontHeight = 20;

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: ");
    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);
      if (cx >= w) {
        // move to next line
        cx = 0;

    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);
      if (cx >= w) {
        // move to next line
        cx = 0;
      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: ");
  } else {
    server.send(500, "text/plain", "500: error");

void setupServer() {
  server.on("/api/rgb", HTTP_POST, []() {
  }, rgbApi);

void setup(void) {


void loop(void) {
  while (WiFi.status() != WL_CONNECTED) {
    hasImage = false;
    ucg.setColor(255, 255, 255);
    ucg.setPrintPos(0, 1 * fontHeight);

  if (!hasImage) {
    ucg.setColor(0, 255, 0);
    ucg.setPrintPos(0, 1 * fontHeight);
    ucg.print("Connected, IP address: ");
    ucg.setPrintPos(0, 2 * fontHeight);


Live Desktop Feed

On macOs you can run the following bash script to send your current desktop to the tiny screen:

# example



while (true);
    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"

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 :).

See my followup post on this.