I have quite a collection of old laptops and tablets and except for taking out battery cells (Lipo and 18650s) i have not done much with the other components. The screen was an obvious candidate, but it does not use any communications protocols compatible with Arduino style microcontrollers (SPI, I2C, UART), many use LVDS with a 40pin flat connector. Luckily there are cheap (US$10) converter boards to VGA and HDMI. I got myself this one, which comes with a button panel (power, menu, up, down, enter) and audio output. The supplier asked for the exact model number of the display i was planning to hook up, maybe these converters are not universal, i have only tried it with one display, this 10.1 inch LCD from a Chinese 2008 Android tablet.

10.1” transparent LCD with LVDS converter to VGA to ESP32, with hill profile in the background
model number of the LCD

I tried the converter+LCD with a Raspberry Pi over HDMI, it worked immediately. Also connected to my Linux laptop it showed up as a third screen. The converter needs a nominal 12V power supply, but it works with 5V as well.

But i was more interested in hooking it up to an ESP32, the popular and cheap wifi microcontroller. With the help of Bitluni’s ESP32Lib library, using the simple 3bit wiring (no extra components), it was easy to get the VGAHelloWorld.ino sketch working.

from top to bottom: LCD driver, LCD, LVDS converter, VGA connector, ESP32

First I used jumer wires to stick into the female VGA connector, and the ESP32 on the breadboard. The converter board was powered with 9V, the ESP32 over USB.

However, i wanted to make things a bit more interesting by removing the LCD’s backlight, to get the fancy effect of a transparent screen and hang it in my window. It was easy to remove the backlight panel, just a few screws, and 2 wires to unsolder. But without the enclosure, the LCD screen is very fragile, careful!

The connectors to the driver board are on the bottom of the screen, and i wanted to use it upside down, fixing the driver board to my window, and the screen hanging down from the driver board. The above library does not have an invert/flip/orientation function (yet), so i had to add a few lines in ESP32Lib-master/src/Graphics/GraphicsR1G1B1A1X2S2Swapped.h

There are 5 functions that need these 2 extra lines: dot, dotFast, dotMix, dotAdd, get; we just need to invert x and y, here is the result for dot():

 virtual void dot(int x, int y, Color color)
 {
 x = xres - x - 1;  // for inverted display
 y = yres - y - 1;  // for inverted display
 if ((unsigned int)x < xres && (unsigned int)y < yres)
    backBuffer[y][x^2] = (color & RGBAXMask) | SBits;
 }

To make the wiring more robust, i cut an old VGA cable to use the male connector (pinout here). I found 3 thick wires inside, the Red, Green, Blue, with their own shielding, but no need to use those GNDs. The thin black wire was HorizontalSync, the yellow one VerticalSync, and white GND. To make things easy, i used adjacent pins on the ESP32, and soldered the connector wires to the top of my ESP32 Dev Module.

shielded Red Green Blue wires
5 GPIO pins on the ESP32, see code

On the LVDS converter board i found pads labelled 5V and GND, so i soldered some jumper wires to connect power to the ESP32 5V and GND pins. Powered by 5V the converter+LCD+ESP32 draw around 370mA (when wifi is not active), less than 2W, not bad for a 10.1” screen i think.

I have quite a few sensors in my house, so i wanted to use this LCD as a dashboard, in the window behind my desk. I host my sensor data at emoncms.org and that has a simple API to retrieve the latest data values of my sensor feeds. I also retrieve the timestamp of the last data point, to decide whether it is recent enough to accept it, if not, it shows up as ‘N/A’. The code also includes some alarm levels per data type, e.g. if the humidity is over 80%, it will show in red.

I also implemented a graph showing recent power total consumption, with the graph moving away to the left, based on a sort of ringbuffer.

And the NTP time is shown in the upper right corner.

Below is the full code, minus credentials.

/*
   BuffaloLabs VGA display on ESP32
   showing sensor data from emoncms.org

   VGA stuff from https://github.com/bitluni/ESP32Lib
   for my old 10.1'' tablet display, we need to invert the image (flip v and h)
   edit library file ESP32Lib-master/src/Graphics/GraphicsR1G1B1A1X2S2Swapped.h add 5 times: dot, dotFast, dotMix, dotAdd, get
    x = xres - x - 1;  // for inverted display
    y = yres - y - 1;  // for inverted display

   Tom Tobback - April 2020
*/

// PARAMETERS
const uint32_t feed_max_age = 10 * 60;        // (sec) if feed value older than this, assume node is offline
const uint32_t retrieve_interval = 60 * 1000; // (msec) between getting data
const int pm25_alarm = 25;
const float bat_alarm = 3.2;
const int power_alarm = 2000;
const int hum_alarm = 80;
const int temp_alarm = 40;
const int wind_alarm = 5;


// VGA stuff from https://github.com/bitluni/ESP32Lib
#include <ESP32Lib.h>
#include <Ressources/Font6x8.h>
#include <Ressources/CodePage437_8x8.h>
#include <Ressources/CodePage437_9x16.h>
VGA3Bit vga;
const uint32_t RED = 0x0000ff;
const uint32_t GREEN = 0x00ff00;
const uint32_t BLUE = 0xff0000;
const uint32_t BLACK = 0x000000;
const uint32_t YELLOW = 0x00ffff;
const int col1 = 100;
void displayTime(int x, int y, int h, int m);
void displayFloat(int x, int y, float v, uint32_t col);
void displayInt(int x, int y, int v, uint32_t col);
void drawPowerGraph();

// WIFI and NTP
#include <WiFi.h>
#include "time.h"
const char* ssid       = "xxx";
const char* password   = "xxx";
const char* ntpServer = "hk.pool.ntp.org";
const long  gmtOffset_sec = 8 * 3600;  // GMT+8
int previous_minute = 0;    // to refresh display
void printLocalTime();

// EMONCMS
#define EMONCMS "emoncms.org"    // emoncms.org
#define APIKEY "xxx"
uint32_t feeds[] = {
  x, // 0 total kW
  x, // 1 humiduino bat
  x, // 2 emontx bat
  x,// 3 pm2.5 gf
  x,// 4 pm2.5 1f
  x,// 5 pm2.5 tung chung
  x,// 6 pm2.5 central
  x,// 7 hum in gf
  x,// 8 hum in roof
  x, // 9 temp out gf
  x, // 10 temp out roof
  x // 11 wind speed
};
float feeds_value[11];        // to store the retrieved values
byte feeds_number = sizeof(feeds) / 4;   // 32-bit = 4-byte
float getFeedValue(uint32_t f);
//int power_log[16] = {0 , 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500};
int power_log[16] = {0};
int power_log_index = 15;
uint32_t last_retrieve_timestamp;

/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);

  Serial.println("======================================");
  Serial.println("==== ESP32 VGA IoT SCREEN ============");
  Serial.println("======================================");

  Serial.print(">>> init VGA..");
  //initializing vga at the specified pins
  vga.init(vga.MODE320x240, redPin, greenPin, bluePin, hsyncPin, vsyncPin);
  //selecting the font
  vga.setFont(CodePage437_9x16);
  //vga.clear(vga.RGB(0xffffff));
  vga.clear(vga.RGBA(255, 255, 255, 255));
  vga.setTextColor(vga.RGB(0));
  //vga.setTextColor(vga.RGB(0xffffff), vga.RGBA(0, 0, 0, 0));
  //vga.setTextColor(vga.RGB(0xffffff), vga.RGBA(0, 0, 0, 255));
  //vga.setTextColor(vga.RGB(0x000000), vga.RGBA(0, 0, 0, 0));
  vga.setCursor(2, 0);
  //displaying the text
  vga.println("Hello Mui Wo!");

  Serial.print(">>> init WIFI..");
  vga.print(">>> init WIFI..");
  WiFi.begin(ssid, password);
  byte counter = 0;
  while (WiFi.status() != WL_CONNECTED && counter < 10) {
    delay(500);
    Serial.print(".");
    vga.print(".");
    counter++;
  }
  if (counter == 10) ESP.restart();
  Serial.println(" connected");
  vga.println(" connected");

  Serial.println(">>> init NTP..");
  vga.println(">>> init NTP..");
  configTime(gmtOffset_sec, 0, ntpServer);
  printLocalTime();

  vga.clear(vga.RGBA(255, 255, 255, 255));
  vga.setTextColor(vga.RGB(0x000000), vga.RGBA(255, 255, 255, 255));

  last_retrieve_timestamp = millis() - retrieve_interval;
}

/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////

void loop() {

  struct tm timeinfo;
  getLocalTime(&timeinfo);
  char timeMinute[3]; // detour to extract minutes
  strftime(timeMinute, 3, "%M", &timeinfo);
  int this_min = atoi(timeMinute);
  if (previous_minute != this_min) {
    printLocalTime();
    char timeHour[3]; // detour to extract hour
    strftime(timeHour, 3, "%H", &timeinfo);
    int this_hour = atoi(timeHour);
    displayTime(275, 0, this_hour, this_min);
    previous_minute = this_min;
  }

  if (millis() - last_retrieve_timestamp > retrieve_interval) {
    for (int i = 0; i < feeds_number; i++) {
      feeds_value[i] = getFeedValue(feeds[i]);
      Serial.print(feeds_value[i]);
      Serial.print("\t");
  vga.setFont(CodePage437_9x16);
      vga.setTextColor(vga.RGB(BLACK), vga.RGBA(255, 255, 255, 255));
      switch (i) {
        case 0:
          Serial.print("total Watt");
          vga.setCursor(2, 0);
          vga.print("Power:");
          if (feeds_value[0] > power_alarm) displayInt(col1, 0, feeds_value[0], RED);
          else displayInt(col1, 0, feeds_value[0], BLACK);
          if (feeds_value[0] != -255) vga.print("W  ");
          if (feeds_value[0] != -255) { // update power_log with latest value
            power_log_index++;
            if (power_log_index == sizeof(power_log) / sizeof(power_log[0])) power_log_index = 0;
            power_log[power_log_index] = feeds_value[0];
            Serial.print("\t index:");
            Serial.print(power_log_index);
            drawPowerGraph();
          }
          break;
        case 1:
          Serial.print("bat humiduino");
          vga.setCursor(2, 20);
          vga.print("Bat 2f:");
          if (feeds_value[1] < bat_alarm) displayFloat(col1, 20, feeds_value[1], RED);
          else displayFloat(col1, 20, feeds_value[1], BLACK);
          if (feeds_value[1] != -255) vga.print("V  ");
          break;
        case 2:
          Serial.print("bat emontx");
          vga.setCursor(2, 35);
          vga.print("Bat gf:");
          if (feeds_value[2] < bat_alarm) displayFloat(col1, 35, feeds_value[2], RED);
          else displayFloat(col1, 35, feeds_value[2], BLACK);
          if (feeds_value[2] != -255) vga.print("V  ");
          break;
        case 3:
          Serial.print("pm2.5 gf");
          vga.setCursor(2, 55);
          vga.print("PM2.5 gf:");
          if (feeds_value[3] > pm25_alarm) displayFloat(col1, 55, feeds_value[3], RED);
          else displayFloat(col1, 55, feeds_value[3], BLACK);
          break;
        case 4:
          Serial.print("pm2.5 1f");
          vga.setCursor(2, 70);
          vga.print("PM2.5 1f:");
          if (feeds_value[4] > pm25_alarm) displayFloat(col1, 70, feeds_value[4], RED);
          else displayFloat(col1, 70, feeds_value[4], BLACK);
          break;
        case 5:
          Serial.print("pm2.5 tung chung");
          vga.setCursor(2, 85);
          vga.print("PM2.5 tc:");
          if (feeds_value[5] > pm25_alarm) displayFloat(col1, 85, feeds_value[5], RED);
          else displayFloat(col1, 85, feeds_value[5], BLACK);
          break;
        case 6:
          Serial.print("pm2.5 central");
          vga.setCursor(2, 100);
          vga.print("PM2.5 ce:");
          if (feeds_value[6] > pm25_alarm) displayFloat(col1, 100, feeds_value[6], RED);
          else displayFloat(col1, 100, feeds_value[6], BLACK);
          break;
        case 7:
          Serial.print("hum in gf");
          vga.setCursor(2, 120);
          vga.print("Hum in gf:");
          if (feeds_value[7] > hum_alarm) displayInt(col1, 120, feeds_value[7], RED);
          else displayInt(col1, 120, feeds_value[7], BLACK);
          if (feeds_value[7] != -255) vga.print("%  ");
          break;
        case 8:
          Serial.print("hum in roof");
          vga.setCursor(2, 135);
          vga.print("Hum in 2f:");
          if (feeds_value[8] > hum_alarm) displayInt(col1, 135, feeds_value[8], RED);
          else displayInt(col1, 135, feeds_value[8], BLACK);
          if (feeds_value[8] != -255) vga.print("%  ");
          break;
        case 9:
          Serial.print("temp out gf");
          vga.setCursor(2, 155);
          vga.print("T out gf:");
          if (feeds_value[9] > temp_alarm) displayInt(col1, 155, feeds_value[9], RED);
          else displayInt(col1, 155, feeds_value[9], BLACK);
          if (feeds_value[9] != -255) vga.print("degC  ");
          break;
        case 10:
          Serial.print("temp out roof");
          vga.setCursor(2, 170);
          vga.print("T out 2f:");
          if (feeds_value[10] > temp_alarm) displayInt(col1, 170, feeds_value[10], RED);
          else displayInt(col1, 170, feeds_value[10], BLACK);
          if (feeds_value[10] != -255) vga.print("degC  ");
          break;
        case 11:
          Serial.print("wind speed");
          vga.setCursor(2, 190);
          vga.print("Wind speed:");
          if (feeds_value[11] > wind_alarm) displayInt(col1, 190, feeds_value[11], RED);
          else displayInt(col1, 190, feeds_value[11], BLACK);
          if (feeds_value[11] != -255) vga.print("m/s");
          break;
      }
      Serial.println();
    }
    Serial.println();
    last_retrieve_timestamp = millis();
  }
  delay(100);
}

/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////

void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

/////////////////////////////////////////////////////////////////////////////////////////

float getFeedValue(uint32_t f) {  // return -255 if value unavailable or too old

  WiFiClient client;
  if (!client.connect(EMONCMS, 80)) {
    Serial.println(" connection to emoncms.org failed");
    return -255;
  }

  String url = "/feed/timevalue.json?apikey=";
  url += APIKEY;
  url += "&id=";
  url += f;

  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: emoncms.org\r\n" +
               "Connection: close\r\n\r\n");
  delay(10);
  //if (debug) Serial.println(url);

  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 5000) {
      Serial.println(">>> Client Timeout !");
      client.stop();
      return -255;
    }
  }

  // the reply is a json with parameters "time" and "value"
  client.find("time");
  uint32_t this_time = client.parseInt();
  client.find("value");
  float this_value = client.parseFloat();
  client.stop();

  time_t now_unix_time;
  time(&now_unix_time);  // in sec
  int this_age = now_unix_time - this_time;
  Serial.print(this_age);
  Serial.print("\t");
  if (this_age < feed_max_age) return this_value;
  else {
    Serial.print("!TOO OLD!\t");
    Serial.print(this_value);
    return -255;
  }
}

/////////////////////////////////////////////////////////////////////////////////////////

void displayTime(int x, int y, int h, int m) {

  vga.setFont(CodePage437_9x16);
  vga.setTextColor(vga.RGB(0x000000), vga.RGBA(255, 255, 255, 255));
  vga.setCursor(x, y);

  if (h < 10) vga.print("0");
  vga.print(h);
  vga.print(":");
  if (m < 10) vga.print("0");
  vga.print(m);
}

/////////////////////////////////////////////////////////////////////////////////////////

void displayFloat(int x, int y, float v, uint32_t col) {
  vga.setFont(CodePage437_9x16);
  vga.setTextColor(vga.RGB(col), vga.RGBA(255, 255, 255, 255));
  vga.setCursor(x, y);
  if (v != -255) vga.print(v, 1);
  else {
    vga.setTextColor(vga.RGB(RED), vga.RGBA(255, 255, 255, 255));
    vga.print("N/A");
  }
}

/////////////////////////////////////////////////////////////////////////////////////////

void displayInt(int x, int y, int v, uint32_t col) {
  vga.setFont(CodePage437_9x16);
  vga.setTextColor(vga.RGB(col), vga.RGBA(255, 255, 255, 255));
  vga.setCursor(x, y);
  if (v != -255) vga.print(v);
  else {
    vga.setTextColor(vga.RGB(RED), vga.RGBA(255, 255, 255, 255));
    vga.print("N/A");
  }
}

/////////////////////////////////////////////////////////////////////////////////////////

void drawPowerGraph() {
  Serial.println();
  int x0 = 180; // upper left
  int y0 = 110; // upper left
  int w = 128;  // width
  int h = 100;  // height
  vga.fillRect(x0, y0, w, h, vga.RGBA(255, 255, 255, 255));   // clear to transparent background
  int len = sizeof(power_log) / sizeof(power_log[0]);
  int bar = w / len;
  for (int i = 0; i < len; i++) {
    int this_index = (power_log_index + i + 1) % len;
    //Serial.println(power_log[this_index]);
    int val = constrain(power_log[this_index] / 100, 0, h);
    vga.fillRect(x0 + bar * i, y0 + h - val, bar, val, vga.RGB(YELLOW));
  }
  vga.rect(x0, y0, w, h, vga.RGB(0));   // draw black rectangle
  vga.setCursor(x0 + 2, y0 + 2);
  vga.setFont(CodePage437_8x8);
  vga.setTextColor(vga.RGB(BLACK), vga.RGBA(255, 255, 255, 255));
  vga.print("power");
  for (int i=0; i< 10; i++) {
    vga.xLine(x0, x0 + 2, y0 + i * h/10, vga.RGB(BLACK));
    vga.xLine(x0 + w - 2, x0 + w, y0 + i * h/10, vga.RGB(BLACK));
  }
}

/////////////////////////////////////////////////////////////////////////////////////////

ESP32 with transparent LCD screen over VGA

One thought on “ESP32 with transparent LCD screen over VGA

Leave a Reply to soap dispenser Cancel reply

Your email address will not be published. Required fields are marked *