ESP32 TFT with LVGL: Display BME280 Sensor Data on a Table

In this guide, you’ll learn how to create a responsive table using LVGL with an ESP32 and a TFT display. We’ll display temperature, humidity, and pressure from the BME280 sensor, and luminosity from the LDR on the display. We’ll also display the timestamp associated with the readings (date and time). The ESP32 will be programmed using Arduino IDE.

ESP32 TFT Screen Display BME280 Table Readings LVGL Arduino

Are you using a CYD board? Read this guide: ESP32 CYD with LVGL – Display BME280 Sensor Data on a Table

Project Overview

In this project, we’ll create a table with sensor readings from the BME280 sensor and the LDR on a 2.8-inch ILI9341 TFT LCD Touchscreen (240×320). We’ll also display the date and time, and your board IP address.

To get an accurate date and time for your timezone, we’ll use the WorldTimeAPI. To get the time from the API, the ESP32 needs to connect to the internet, so you need to have a router in your surroundings so that the ESP32 can connect to it.

If you don’t have an internet connection where the ESP32 will operate, you can omit the date and time section or use an RTC module.

We’ll also display a floating button with the refresh icon that when clicked will update the values on the table.

Prerequisites

Before proceeding, make sure you follow the next prerequisites. You must follow all steps, otherwise, your project will not work.

1) Parts Required

For this project, you need the following parts:

2) Install ESP32 Boards in Arduino IDE

Arduino IDE 2 Logo

We’ll program the ESP32 using Arduino IDE. Make sure you have the ESP32 boards installed. Follow the next tutorial:

3) Get familiar with the ILI9341 TFT LCD Touchscreen Display

The display we’re using in this guide is the 2.8. inch TFT LCD that also comes with a touchscreen. The display communicates via SPI communication protocol and uses the ILI9341 driver. The touchscreen also uses the SPI communication protocol.

The TFT LCD touchscreen also comes with an SD card interface if you need to load files for your specific project. This display is also available with different screen sizes, but we’ll use the one with 240 x 320 pixels).

ILI9341 TFT LCD Touchscreen Display

If this is your first time using this display, make sure to follow our getting started guide:

4) Wire the Display to the ESP32

Wire the TFT LCD and touchscreen pins to the ESP32 GPIOs according to the next table (you must use these exact pins, otherwise the project will not work).

Wiring TFT LCD Touchscreen display to ESP32
TFT LCD TouchscreenESP32
T_IRQGPIO 36
T_OUTGPIO 39
T_DINGPIO 32
T_CSGPIO 33
T_CLKGPIO 25
SDO(MISO)GPIO 12
LEDGPIO 21
SCKGPIO 14
SDI(MOSI)GPIO 13
D/CGPIO 2
RESETEN/RESET
CSGPIO 15
GNDGND
VCC5V (or 3.3V)*

* In the VCC pin, you can either use 5V or 3.3V depending if your J1 connection is open or closed (by default it’s usually open as you can see in the figure below).

VCC = 5V | J1=OPEN
VCC = 3.3V | J1=CLOSE
TFT LCD Touchscreen display J1 connection

5) Install TFT and LVGL Libraries

LVGL (Light and Versatile Graphics Library) is a free and open-source graphics library that provides a wide range of easy-to-use graphical elements for your microcontroller projects that require a graphical user interface (GUI).

LVGL new logo

Follow the next tutorial to install and configure the required libraries to use LVGL with the 2.8 inch ILI9341 240×320 TFT LCD Touchscreen using Arduino IDE.

6) Install ArduinoJson and BME280 Libraries

For this project, you need to install the ArduinoJSON library to handle the JSON response when you make a request to the WorldTimeAPI.

In the Arduino IDE, go to Sketch > Include Library > Manage Libraries. Search for ArduinoJSON and install the library by Benoit Blanchon. We’re using version 7.0.4. We recommend using the same version.

We’ll use the Adafruit BME280 library to get data from the BME280. In the Arduino IDE, go to Sketch > Include Library > Manage Libraries. Search for Adafruit BME280 Library on the search box and install the library. Also, install any dependencies currently not installed (usually the Adafruit Bus IO and the Adafruit Unified Sensor libraries).

Installing Adafruit BME280 Sensor Library Arduino IDE

ESP32 TFT: Display BME280 Sensor Readings on a Table – Arduino Code

The following code will create the table and display the sensor readings. Before uploading the code to your board, you need to insert your network credentials so that the ESP32 can connect to the internet to get the time. You also need to insert your timezone.

/*  Rui Santos & Sara Santos - Random Nerd Tutorials - https://RandomNerdTutorials.com/esp32-cyd-lvgl-display-bme280-data-table/   |   https://RandomNerdTutorials.com/esp32-tft-lvgl-display-bme280-data-table/
    THIS EXAMPLE WAS TESTED WITH THE FOLLOWING HARDWARE:
    1) ESP32-2432S028R 2.8 inch 240×320 also known as the Cheap Yellow Display (CYD): https://makeradvisor.com/tools/cyd-cheap-yellow-display-esp32-2432s028r/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/cyd-lvgl/
    2) REGULAR ESP32 Dev Board + 2.8 inch 240x320 TFT Display: https://makeradvisor.com/tools/2-8-inch-ili9341-tft-240x320/ and https://makeradvisor.com/tools/esp32-dev-board-wi-fi-bluetooth/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/esp32-tft-lvgl/
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/

/*  Install the "lvgl" library version 9.X by kisvegabor to interface with the TFT Display - https://lvgl.io/
    *** IMPORTANT: lv_conf.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE lv_conf.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <lvgl.h>

/*  Install the "TFT_eSPI" library by Bodmer to interface with the TFT Display - https://github.com/Bodmer/TFT_eSPI
    *** IMPORTANT: User_Setup.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE User_Setup.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <TFT_eSPI.h>

// Install the "XPT2046_Touchscreen" library by Paul Stoffregen to use the Touchscreen - https://github.com/PaulStoffregen/XPT2046_Touchscreen - Note: this library doesn't require further configuration
#include <XPT2046_Touchscreen.h>

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Specify the timezone you want to get the time for
const char* timezone = "Europe/Lisbon";

// Store date and time
String current_date;
String current_time;

// Install Adafruit Unified Sensor and Adafruit BME280 Library
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define I2C_SDA 27
#define I2C_SCL 22
#define SEALEVELPRESSURE_HPA (1013.25)
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

// SET VARIABLE TO 0 FOR TEMPERATURE IN FAHRENHEIT DEGREES
#define TEMP_CELSIUS 1    

#define LDR_PIN 34

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320

// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;

#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

// If logging is enabled, it will inform the user about what is happening in the library
void log_print(lv_log_level_t level, const char * buf) {
  LV_UNUSED(level);
  Serial.println(buf);
  Serial.flush();
}

// Get the Touchscreen data
void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) {
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z)
  if(touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();

    // Advanced Touchscreen calibration, LEARN MORE » https://RandomNerdTutorials.com/touchscreen-calibration/
    float alpha_x, beta_x, alpha_y, beta_y, delta_x, delta_y;

    // REPLACE WITH YOUR OWN CALIBRATION VALUES » https://RandomNerdTutorials.com/touchscreen-calibration/
    alpha_x = -0.000;
    beta_x = 0.090;
    delta_x = -33.771;
    alpha_y = 0.066;
    beta_y = 0.000;
    delta_y = -14.632;

    x = alpha_y * p.x + beta_y * p.y + delta_y;
    // clamp x between 0 and SCREEN_WIDTH - 1
    x = max(0, x);
    x = min(SCREEN_WIDTH - 1, x);

    y = alpha_x * p.x + beta_x * p.y + delta_x;
    // clamp y between 0 and SCREEN_HEIGHT - 1
    y = max(0, y);
    y = min(SCREEN_HEIGHT - 1, y);

    // Basic Touchscreen calibration points with map function to the correct width and height
    //x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    //y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);

    z = p.z;

    data->state = LV_INDEV_STATE_PRESSED;

    // Set the coordinates
    data->point.x = x;
    data->point.y = y;

    // Print Touchscreen info about X, Y and Pressure (Z) on the Serial Monitor
    Serial.print("X = ");
    Serial.print(x);
    Serial.print(" | Y = ");
    Serial.print(y);
    Serial.print(" | Pressure = ");
    Serial.print(z);
    Serial.println();
  }
  else {
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

static void float_button_event_cb(lv_event_t * e) {
  update_table_values();
}

static lv_obj_t * table;
static void update_table_values(void) {
  // Get the latest temperature reading in Celsius or Fahrenheit
  #if TEMP_CELSIUS
    float bme_temp = bme.readTemperature();
    const char degree_symbol[] = "\u00B0C";
  #else
    float bme_temp = 1.8 * bme.readTemperature() + 32;
    const char degree_symbol[] = "\u00B0F";
  #endif
  
  String bme_temp_value = String(bme_temp) + degree_symbol;
  String bme_humi_value = String(bme.readHumidity()) + "%";
  String bme_press_value = String(bme.readPressure() / 100.0F) + " hPa";
  String ldr_value = String(analogRead(LDR_PIN));
  
  // Get the time from WorldTimeAPI
  get_date_and_time();
  //Serial.println("Current Date: " + current_date);
  //Serial.println("Current Time: " + current_time);

  // Fill the first column
  lv_table_set_cell_value(table, 0, 0, "Data");
  lv_table_set_cell_value(table, 1, 0, "Temperature");
  lv_table_set_cell_value(table, 2, 0, "Humidity");
  lv_table_set_cell_value(table, 3, 0, "Pressure");
  lv_table_set_cell_value(table, 4, 0, "Luminosity");
  lv_table_set_cell_value(table, 5, 0, "Date");
  lv_table_set_cell_value(table, 6, 0, "Time");
  lv_table_set_cell_value(table, 7, 0, "IP Address");

  // Fill the second column
  lv_table_set_cell_value(table, 0, 1, "Value");
  lv_table_set_cell_value(table, 1, 1, bme_temp_value.c_str());
  lv_table_set_cell_value(table, 2, 1, bme_humi_value.c_str());
  lv_table_set_cell_value(table, 3, 1, bme_press_value.c_str());
  lv_table_set_cell_value(table, 4, 1, ldr_value.c_str());
  lv_table_set_cell_value(table, 5, 1, current_date.c_str());
  lv_table_set_cell_value(table, 6, 1, current_time.c_str());
  lv_table_set_cell_value(table, 7, 1, WiFi.localIP().toString().c_str());
}

static void draw_event_cb(lv_event_t * e) {
  lv_draw_task_t * draw_task = lv_event_get_draw_task(e);
  lv_draw_dsc_base_t * base_dsc = (lv_draw_dsc_base_t*) draw_task->draw_dsc;
  // If the cells are drawn
  if(base_dsc->part == LV_PART_ITEMS) {
    uint32_t row = base_dsc->id1;
    uint32_t col = base_dsc->id2;

    // Make the texts in the first cell center aligned
    if(row == 0) {
      lv_draw_label_dsc_t * label_draw_dsc = lv_draw_task_get_label_dsc(draw_task);
      if(label_draw_dsc) {
        label_draw_dsc->align = LV_TEXT_ALIGN_CENTER;
      }
      lv_draw_fill_dsc_t * fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task);
      if(fill_draw_dsc) {
        fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), fill_draw_dsc->color, LV_OPA_20);
        fill_draw_dsc->opa = LV_OPA_COVER;
      }
    }
    // In the first column align the texts to the right
    else if(col == 0) {
      lv_draw_label_dsc_t * label_draw_dsc = lv_draw_task_get_label_dsc(draw_task);
      if(label_draw_dsc) {
        label_draw_dsc->align = LV_TEXT_ALIGN_RIGHT;
      }
    }

    // Make every 2nd row gray color
    if((row != 0 && row % 2) == 0) {
      lv_draw_fill_dsc_t * fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task);
      if(fill_draw_dsc) {
        fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_GREY), fill_draw_dsc->color, LV_OPA_10);
        fill_draw_dsc->opa = LV_OPA_COVER;
      }
    }
  }
}

void lv_create_main_gui(void) {
  table = lv_table_create(lv_screen_active());

  // Inserts or updates all table values
  update_table_values();

  // Set a smaller height to the table. It will make it scrollable
  lv_obj_set_height(table, 200);
  lv_obj_center(table);

  // Add an event callback to apply some custom drawing
  lv_obj_add_event_cb(table, draw_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
  lv_obj_add_flag(table, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS);

  // Create floating button
  lv_obj_t * float_button = lv_button_create(lv_screen_active());
  lv_obj_set_size(float_button, 50, 50);
  lv_obj_add_flag(float_button, LV_OBJ_FLAG_FLOATING);
  lv_obj_align(float_button, LV_ALIGN_BOTTOM_RIGHT, -15, -15);
  lv_obj_add_event_cb(float_button, float_button_event_cb, LV_EVENT_CLICKED, NULL);
  lv_obj_set_style_radius(float_button, LV_RADIUS_CIRCLE, 0);
  lv_obj_set_style_bg_image_src(float_button, LV_SYMBOL_REFRESH, 0);
  lv_obj_set_style_text_font(float_button, lv_theme_get_font_large(float_button), 0);
  lv_obj_set_style_bg_color(float_button, lv_palette_main(LV_PALETTE_GREEN), LV_PART_MAIN);
}

void get_date_and_time() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;

    // Construct the API endpoint
    String url = String("http://worldtimeapi.org/api/timezone/") + timezone;

    http.begin(url);

    int httpCode = http.GET(); // Make the GET request

    if (httpCode > 0) {
      // Check for the response
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        //Serial.println("Time information:");
        //Serial.println(payload);
        
        // Parse the JSON to extract the time
        JsonDocument doc;
        DeserializationError error = deserializeJson(doc, payload);

        if (!error) {
          const char* datetime = doc["datetime"];
          //Serial.println("Datetime: " + String(datetime));
          
          // Split the datetime into date and time
          String datetimeStr = String(datetime);
          int splitIndex = datetimeStr.indexOf('T');
          current_date = datetimeStr.substring(0, splitIndex);
          current_time = datetimeStr.substring(splitIndex + 1, splitIndex + 9); // Extract time portion

        } else {
          Serial.print("deserializeJson() failed: ");
          Serial.println(error.c_str());
        }
      }
    } else {
      Serial.printf("GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end(); // Close connection
  } else {
    Serial.println("Not connected to Wi-Fi");
  }
}

void setup() {
  String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
  Serial.begin(115200);
  Serial.println(LVGL_Arduino);

  // Set analog read resolution
  analogReadResolution(12);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.print("\nConnected to Wi-Fi network with IP Address: ");
  Serial.println(WiFi.localIP());

  I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
  bool status;
  // Passing a &Wire2 to set custom I2C ports
  status = bme.begin(0x76, &I2CBME);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
  
  // Start LVGL
  lv_init();
  // Register print function for debugging
  lv_log_register_print_cb(log_print);

  // Start the SPI for the touchscreen and init the touchscreen
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  // Set the Touchscreen rotation in landscape mode
  // Note: in some displays, the touchscreen might be upside down, so you might need to set the rotation to 0: touchscreen.setRotation(0);
  touchscreen.setRotation(2);

  // Create a display object
  lv_display_t * disp;
  // Initialize the TFT display using the TFT_eSPI library
  disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
  lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
  
  // Initialize an LVGL input device object (Touchscreen)
  lv_indev_t * indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  // Set the callback function to read Touchscreen input
  lv_indev_set_read_cb(indev, touchscreen_read);

  // Function to draw the GUI
  lv_create_main_gui();
}

void loop() {
  lv_task_handler();  // let the GUI do its work
  lv_tick_inc(5);     // tell LVGL how much time has passed
  delay(5);           // let this time pass
}

View raw code

How Does the Code Work?

Let’s see how to get the time from the internet and display all data on a table. Alternatively, you can skip to the Demonstration section.

Including Libraries

You need to include the lvgl, TFT_eSPI, and the XPT2046_Touchscreen libraries to communicate and display text on the screen.

#include <lvgl.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>

You need to include the WiFi, HTTPClient, and the ArduinoJson libraries to make HTTP requests and handle JSON data.

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

To use the BME280, we need to include the following libraries.

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Insert Your Credentials and Timezone

In the following lines, you must insert your network credentials so that the ESP32 can connect to your router.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Set your timezone in the timezone variable at the beginning of the code (list of all available timezones).

const char* timezone = "Europe/Lisbon";

Declaring Other Variables

We need to create an I2C instance on custom I2C pins and an Adafruit_BME280 object to refer to the sensor.

#define I2C_SDA 27
#define I2C_SCL 22
#define SEALEVELPRESSURE_HPA (1013.25)
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

Our code is prepared to display the temperature in Celsius or Fahrenheit degrees. To choose your desired Unit, you can set the value of the TEMP_CELSIUS variable. It is set to 1 by default to display the temperature in Celsius degrees.

#define TEMP_CELSIUS 1

If you want to display in Fahrenheit degrees instead, set it to 0.

#define TEMP_CELSIUS 0

Create a LVGL table object, so that we can access it inside all functions later on.

static lv_obj_t * table;

setup()

In the setup(), include the following lines for debugging. These will print the version of LVGL that you’re using. You must be using version 9.

String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);

Connect to the Internet

To connect the ESP32 to the internet we use the following code.

// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
Serial.print("\nConnected to Wi-Fi network with IP Address: ");
Serial.println(WiFi.localIP());

Init BME280

The following lines initialize the sensor on the I2C bus created:

I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
bool status;
// Passing a &Wire2 to set custom I2C ports
status = bme.begin(0x76, &I2CBME);
if (!status) {
  Serial.println("Could not find a valid BME280 sensor, check wiring!");
  while (1);
}  

Initialize the LVGL Library

Initialize the LVGL Library by calling the lv_init() function in the setup().

// Start LVGL
lv_init();

Register Debugging Function

Register your log_print() function declared previously as a function associated with debugging LVGL.

// Register print function for debugging
lv_log_register_print_cb(log_print);

Preparing the Touchscreen

Initialize the touchscreen.

// Start the SPI for the touchscreen and init the touchscreen
touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreenSPI);
// Set the Touchscreen rotation in landscape mode
// Note: in some displays, the touchscreen might be upside down
// so you might need to set the rotation to 0: touchscreen.setRotation(0);
touchscreen.setRotation(2);

Touchscreen Calibration

On the touchscreen_read() function, we adjust the touchscreen points to calibrate them with the display. You must insert the calibration values on the following lines. Check this tutorial to get the calibration values for your display.

// REPLACE WITH YOUR OWN CALIBRATION VALUES » https://RandomNerdTutorials.com/touchscreen-calibration/
alpha_x = -0.000;
beta_x = 0.090;
delta_x = -33.771;
alpha_y = 0.066;
beta_y = 0.000;
delta_y = -14.632;

Create a Display Object

To write to the display, you must create a display object first. You need to do this in all your LVGL sketches. The following lines will create an LVGL display object called disp with the screen width, screen height, and drawing buffer defined earlier.

// Create a display object
lv_display_t * disp;
// Initialize the TFT display using the TFT_eSPI library
disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);

Drawing the GUI

The LVGL library works asynchronously. You must call the function to draw on the display in the setup(). Then, everything works with events and callbacks. The code will always be listening for events in the background. When something happens, it will run the callback function associated with the event. You don’t need to check for any events in the loop().

Throughout most of our examples, the function that will draw to the screen will be called lv_create_main_gui(). Then, inside that function, we’ll add the instructions to build the interface.

// Function to draw the GUI
lv_create_main_gui();

get_date_and_time()

We create two global variables current_date and current_time at the beginning of the code to save the current date and time.

String current_date;
String current_time;

Create a function called get_date_and_time() that makes a request to the WorldTimeAPI and updates the current_date and current_time variables with the current date and time.

void get_date_and_time() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;

    // Construct the API endpoint
    String url = String("http://worldtimeapi.org/api/timezone/") + timezone;
    http.begin(url);
    int httpCode = http.GET(); // Make the GET request

    if (httpCode > 0) {
      // Check for the response
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        //Serial.println("Time information:");
        //Serial.println(payload);
        // Parse the JSON to extract the time
        JsonDocument doc;
        DeserializationError error = deserializeJson(doc, payload);
        if (!error) {
          const char* datetime = doc["datetime"];          
          // Split the datetime into date and time
          String datetime_str = String(datetime);
          int splitIndex = datetime_str.indexOf('T');
          current_date = datetime_str.substring(0, splitIndex);
          current_time = datetime_str.substring(splitIndex + 1, splitIndex + 9); // Extract time portion
          hour = current_time.substring(0, 2).toInt();
          minute = current_time.substring(3, 5).toInt();
          second = current_time.substring(6, 8).toInt();
        } else {
          Serial.print("deserializeJson() failed: ");
          Serial.println(error.c_str());
        }
      }
    } else {
      Serial.printf("GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
      sync_time_date = true;
    }
    http.end(); // Close connection
  } else {
    Serial.println("Not connected to Wi-Fi");
  }
}

We won’t cover how HTTP requests work. If you want to learn more, you can check the following tutorial: ESP32 HTTP GET with Arduino IDE.

Preparing the GUI

We create the table inside the lv_create_main_gui() function.

void lv_create_main_gui(void) {

We can create a table object using the lv_table_create() function as follows.

table = lv_table_create(lv_screen_active());

Next, we write a function called update_table_values() that fills the table with the readings. We’ll take a look at that function in just a moment.

update_table_values();

Set the table height and center it on the screen.

// Set a smaller height to the table. It will make it scrollable
lv_obj_set_height(table, 200);
lv_obj_center(table);                                                                          

Add a callback and a flag to the table. Those will allow us to draw the table with a specific style (different colors for the headers and alternating grey and white rows).

// Add an event callback to apply some custom drawing
lv_obj_add_event_cb(table, draw_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
lv_obj_add_flag(table, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS);

Creating the Floating Button

Still, inside the lv_create_main_gui() function, we create the floating button. This is a regular button, but it’s circular and with a specific style.

// Create floating button
lv_obj_t * float_button = lv_button_create(lv_screen_active());
lv_obj_set_size(float_button, 50, 50);
lv_obj_add_flag(float_button, LV_OBJ_FLAG_FLOATING);
lv_obj_align(float_button, LV_ALIGN_BOTTOM_RIGHT, -15, -15);
lv_obj_add_event_cb(float_button, float_button_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_set_style_radius(float_button, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_image_src(float_button, LV_SYMBOL_REFRESH, 0);
lv_obj_set_style_text_font(float_button, lv_theme_get_font_large(float_button), 0);
lv_obj_set_style_bg_color(float_button, lv_palette_main(LV_PALETTE_GREEN), LV_PART_MAIN);

The button when clicked will trigger the float_button_event_cb function.

lv_obj_add_event_cb(float_button, float_button_event_cb, LV_EVENT_CLICKED, NULL);

To add the refresh symbol to the button, we use the lv_obj_set_style_bg_image_src() function. To add the refresh symbol we need to use LV_SYMBOL_REFRESH.

lv_obj_set_style_bg_image_src(float_button, LV_SYMBOL_REFRESH, 0);

Adding Data to the Table

In the update_table_values() is where we’ll add some data to the table.

static void update_table_values(void) {

This function is called when we create the GUI in the lv_create_main_gui() function, and when you click the button on the float_button_event_cb() function.

First, we start by getting readings from the BME280 sensor as well as luminosity from the LDR.

// Get the latest temperature reading in Celsius or Fahrenheit
#if TEMP_CELSIUS
  float bme_temp = bme.readTemperature();
  const char degree_symbol[] = "\u00B0C";
#else
  float bme_temp = 1.8 * bme.readTemperature() + 32;
  const char degree_symbol[] = "\u00B0F";
#endif
  
String bme_temp_value = String(bme_temp) + degree_symbol;
String bme_humi_value = String(bme.readHumidity()) + "%";
String bme_press_value = String(bme.readPressure() / 100.0F) + " hPa";
String ldr_value = String(analogRead(LDR_PIN));

The readings are saved on the bme_temp_value, bme_humi_value, bme_pres_value and ldr_value variables. We update the time variables by calling the get_date_and_time() function that we’ve seen previously.

// Get the time from WorldTimeAPI
get_date_and_time();

After getting all the data, we can finally start adding rows to the table. For that, we can use the lv_table_set_cell_value() function. This function accepts as argument the table you’re referring to, the row number, the column number, and the data you want to display.

The following lines fill the first column.

// Fill the first column
lv_table_set_cell_value(table, 0, 0, "Data");
lv_table_set_cell_value(table, 1, 0, "Temperature");
lv_table_set_cell_value(table, 2, 0, "Humidity");
lv_table_set_cell_value(table, 3, 0, "Pressure");
lv_table_set_cell_value(table, 4, 0, "Luminosity");
lv_table_set_cell_value(table, 5, 0, "Date");
lv_table_set_cell_value(table, 6, 0, "Time");
lv_table_set_cell_value(table, 7, 0, "IP Address");

Finally, we fill the second column with the values from the sensors, the date and time, and the ESP32 IP address.

// Fill the second column
lv_table_set_cell_value(table, 0, 1, "Value");
lv_table_set_cell_value(table, 1, 1, bme_temp_value.c_str());
lv_table_set_cell_value(table, 2, 1, bme_humi_value.c_str());
lv_table_set_cell_value(table, 3, 1, bme_press_value.c_str());
lv_table_set_cell_value(table, 4, 1, ldr_value.c_str());
lv_table_set_cell_value(table, 5, 1, current_date.c_str());
lv_table_set_cell_value(table, 6, 1, current_time.c_str());
lv_table_set_cell_value(table, 7, 1, WiFi.localIP().toString().c_str());

loop()

In the loop(), you can add any other tasks that you need your ESP32 to do like in any regular Arduino sketch.

void loop() {
  lv_task_handler();   // let the GUI do its work
  lv_tick_inc(5);         // tell LVGL how much time has passed
  delay(5);                 // let this time pass
}

Demonstration

Upload the code to your board. Go to Tools > Board and select ESP32 > ESP32 Dev Module. Then, select the right COM port in Tools > Port.

If you see an Error like this: “Sketch too big” during the uploading process, in Arduino IDE go to Tools > Partition scheme > choose anything that has more than 1.4MB APP, for example: “Huge APP (3MB No OTA/1MB SPIFFS“.

Select Huge App Partion Scheme Arduino IDE Tools Menu

Finally, click the upload button.

Arduino IDE 2 Upload Button

After a few seconds, it will display the current data on the screen as shown in the picture below. To update the data on the table, you just need to click on the refresh button.

ESP32 TFT Display BME280 Sensor Data Table Demonstration Refresh Update Values

You can cover the LDR with your finger to see the luminosity values changing.

ESP32 TFT Display BME280 Sensor Data Table Demonstration LDR Sensor

The table is scrollable. You can scroll down to check the date and time of the last update. 

ESP32 TFT Display BME280 Sensor Data Table Demonstration Testing

Troubleshooting

If you are experiencing issues opening the WorldTimeAPI website or if your HTTP request is failing to retrieve time, you can change the following line 252.

String url = String("http://worldtimeapi.org/api/timezone/") + timezone;

From their domain name (worldtimeapi.org) to their IP address as follows:

String url = String("http://213.188.196.246/api/timezone/") + timezone;

Wrapping Up

In this tutorial, you learned to display sensor data in a responsive LVGL table on a TFT with ESP32 board.

We hope you found this tutorial useful. We’re preparing more guides about this board, so stay tuned. If you would like to learn more about creating graphical user interfaces using the LVGL library with the ESP32, check out our latest eBook:

Other guides you might like reading:

To learn more about the ESP32, make sure to take a look at our resources:



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

4 thoughts on “ESP32 TFT with LVGL: Display BME280 Sensor Data on a Table”

  1. You are awesome with this tutorial. I have modified the code to simply show Temperature, Humidity, Pressure, Date and Time. I adjusted the table and removed the heading at the top. I increased the table size to 220, removed the refresh button and added a refresh every 1 minute. I just want to use it for a table top display, but I sure did learn a lot.

    I would like the font to be a little bigger, how would I do that and where would it be in the code?

    Reply
  2. Hi, everythings works fine besides the date and time. On the display both boxes are empty and I get a ‘GET request failure’. The content of the httpCode integer is for both api addresses -5. I have no idea what to do. I installed all libs as required. Can you please support me ? Kind regards Dieter

    Reply
  3. Newbie here, followed guide installed everything as far I know correctly.. Thought it was odd that for the User and lvgl library d/l links the files didn’t have .h after the file.. I renamed them.. But when I verify the script I keep getting

    C:\Users###\AppData\Local\Temp.arduinoIDE-unsaved2024107-28132-5fi99r.gn2y3\sketch_nov7a\sketch_nov7a.ino:15:10: fatal error: lvgl.h: No such file or directory
    15 | #include <lvgl.h>
    | ^~~~~~~~
    compilation terminated.
    exit status 1

    Compilation error: lvgl.h: No such file or directory

    Any info on where I went wrong would be greatly appreciated..

    Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.