Managing Multiple Screens in an LVGL-Based Thermostat Application (Arduino & ESP32)

January 05, 2025

Introduction

In this tutorial, we'll develop a simple thermostat application UI using LVGL on an ESP32. The focus will be on managing multiple screens: a Home Screen and a Settings Screen. We'll walk through the code step-by-step, explaining how to create the user interface (UI) elements, manage screen transitions, and efficiently handle multiple screens.

By the end, you will have a working application where you can view the current temperature, adjust the target temperature, and seamlessly switch between screens.

Multiple Screen Management Overview

In this application, we create both screens (Home and Settings) at the start, and only switch between them when necessary. There are two main approaches for managing multiple screens:

1. Create and Destroy Screens on Demand

In this approach, we create each screen only when we need it, and destroy it when switching to another screen to save memory.

This helps reduce memory usage but might introduce delays when switching between complex screens with many elements.

Here’s a simple example:

lv_obj_t *home_screen;

void create_home_screen() {
    // Create the Home screen dynamically when needed
    home_screen = lv_obj_create(NULL);
    lv_obj_t *temp_label = lv_label_create(home_screen);
    lv_label_set_text_fmt(temp_label, "Current Temp: %d C", current_temp);
    // Load the screen
    lv_scr_load(home_screen);
}

void load_settings_screen() {
    // Destroy the Home screen to free memory
    lv_obj_del(home_screen);

    // Create the Settings screen dynamically when needed
    lv_obj_t *settings_screen = lv_obj_create(NULL);

    lv_obj_t *setpoint_label = lv_label_create(settings_screen);
    lv_label_set_text_fmt(setpoint_label, "Set point: %d C", target_temp);

    // Load the screen
    lv_scr_load(settings_screen);
}

void back_to_home_screen() {
    // Destroy the Settings screen to free memory
    lv_obj_del(lv_scr_act());

    // Re-create and load the Home screen
    create_home_screen();
}

Pros:

  • Reduces memory usage by destroying unused screens.

Cons:

  • Can introduce delays due to screen re-creation, especially for complex screens with many UI elements.

2. Pre-create Screens and Switch (Approach used in this tutorial)

In this approach, we create both screens at the start and switch between them without destroying or reloading the screens. This allows for faster screen transitions but uses more memory.

Here’s how we implement this approach:

void ui_init() {
    // Initialize both screens at the start
    ui_create_home_screen();
    ui_create_settings_screen();

    // Load the home screen
    lv_scr_load(home_screen);
}

void ui_create_home_screen() {
    home_screen = lv_obj_create(NULL);

    // Create and attach UI elements for home screen
    lv_obj_t *temp_label = lv_label_create(home_screen);
    lv_label_set_text_fmt(temp_label, "Current Temp: %d C", current_temp);
}

void ui_create_settings_screen() {
    settings_screen = lv_obj_create(NULL);

    // Create and attach UI elements for settings screen
    setpoint_label = lv_label_create(settings_screen);
    lv_label_set_text_fmt(setpoint_label, "Set point: %d C", target_temp);
}

void settigns_btn_event_handler(lv_event_t * e) {
    // Switch to settings screen without re-creating it
    lv_scr_load(settings_screen);
}

void home_btn_event_handler(lv_event_t * e) {
    // Switch back to home screen without destroying
    lv_scr_load(home_screen);
}

Pros:

  • Faster screen switching, no need to re-create UI elements.
  • No delays when switching screens.

Cons:

  • Uses more memory since all screens are stored in memory even if not in use.

By the end of the day, for smaller embedded systems like the ESP32, it is usually better to pre-create screens to ensure smooth transitions, as demonstrated in this tutorial.

Thermostat Application Example: UI Elements and Screen Navigation

In this example, we have two screens: a Home Screen and a Settings Screen. These screens are part of our simple thermostat application that displays the current temperature and allows the user to adjust the target setpoint.

image.png

UI Elements

  1. Home Screen:

    • Label (temp_label): Displays the current temperature in Celsius, for example, "Current Temp: 25°C".
    • Button (settings_btn): A button labeled "Settings" that, when clicked, navigates the user to the Settings Screen.
  2. Settings Screen:

    • Label (setpoint_label): Shows the current target temperature setpoint, for example, "Setpoint: 30°C".
    • Slider (slider): Allows the user to adjust the temperature setpoint between 16°C and 30°C.
    • Button (home_btn): A button labeled "Home" that, when clicked, returns the user to the Home Screen.

Screen Navigation

Let's break down the flow and interactions based on the following diagram:

image.png

  1. Initial Setup (Step 1):

    • Both the Home Screen and Settings Screen are created and initialized when the application starts.
    • The application initially loads the Home Screen (shown on the left of the diagram).
  2. Interaction with the Home Screen (Step 2):

    • The user interacts with the Home Screen by clicking the Settings button (settings_btn).
    • This triggers the event handler settings_btn_event_handler.
  3. Switch to the Settings Screen (Step 3 & 4):

    • Clicking the Settings button**, t**he LVGL event system invokes the callback functions (settings_btn_event_handler), which handle screen navigation.
    • Inside the event handler, the screen is switched to the Settings Screen by calling lv_scr_load(settings_screen).
  4. Interaction with the Settings Screen (Step 5):

    • The user interacts with the Settings Screen, adjusting the slider to change the setpoint temperature.
    • After adjusting, they can return to the Home Screen by clicking the Home button (home_btn).
  5. Switch Back to Home Screen (Step 6 & 7):

    • Clicking the Home button, the LVGL event system invokes the callback functions triggers the home_btn_event_handler.
    • This event handler switches the screen back to the Home Screen using lv_scr_load(home_screen).

Code Implementation

Next, let's dive into the step-by-step code implementation of this navigation logic using LVGL on the ESP32 platform.

Overview of Screen Management in LVGL

LVGL allows us to create multiple screens and load them as needed. Each screen can hold various UI elements like labels, buttons, sliders, etc. As we discussed in the previous sections, in this example, we’ll:

  • Create two screens: one for the home screen and another for settings.
  • Use buttons to switch between these screens.
  • Manage resources efficiently by creating both screens at the start and loading them as needed.

The basic flow of the code looks like this:

  1. Initialize LVGL and create a theme.
  2. Create both the Home Screen and the Settings Screen.
  3. Use button events to switch between screens.

Creating the Screens

We’ll define two functions: ui_create_home_screen() and ui_create_settings_screen() to create the home and settings screens respectively.

Home Screen

The Home Screen will display the current temperature and have a button to navigate to the Settings Screen.

void ui_create_home_screen()
{
  home_screen = lv_obj_create(NULL); // Create a blank screen

  // Create a label to show the current temperature
  lv_obj_t *temp_label = lv_label_create(home_screen);
  lv_label_set_text_fmt(temp_label, "Current Temp: %d C", current_temp); // Set temperature text
  lv_obj_align(temp_label, LV_ALIGN_CENTER, 0, -50); // Align label

  // Create a button to go to the settings screen
  lv_obj_t *settings_btn = lv_btn_create(home_screen);
  lv_obj_align(settings_btn, LV_ALIGN_CENTER, 0, 50); // Align button
  lv_obj_t *settings_btn_label = lv_label_create(settings_btn);
  lv_label_set_text(settings_btn_label, "Settings"); // Set button text
  lv_obj_add_event_cb(settings_btn, settigns_btn_event_handler, LV_EVENT_CLICKED, NULL); // Add event handler
}
  • We create the home_screen object.
  • A label displays the current temperature.
  • A button is placed to navigate to the Settings Screen using the settigns_btn_event_handler.

Settings Screen

The Settings Screen allows the user to adjust the target temperature using a slider and provides a button to go back to the Home Screen.

void ui_create_settings_screen()
{
  settings_screen = lv_obj_create(NULL); // Create a blank screen

  // Create a label to display the target temperature
  setpoint_label = lv_label_create(settings_screen);
  lv_label_set_text_fmt(setpoint_label, "Set point: %d C", target_temp);
  lv_obj_align(setpoint_label, LV_ALIGN_CENTER, 0, -50); // Align label

  // Create a slider to change the target temperature
  lv_obj_t * slider = lv_slider_create(settings_screen);
  lv_slider_set_range(slider, 16, 30); // Set slider range
  lv_slider_set_value(slider, target_temp, LV_ANIM_OFF); // Set initial slider value
  lv_obj_align(slider, LV_ALIGN_CENTER, 0, 50); // Align slider
  lv_obj_set_width(slider, 180); // Set slider width
  lv_obj_add_event_cb(slider, slider_event_handler, LV_EVENT_VALUE_CHANGED, NULL); // Add event handler

  // Create a button to go back to the home screen
  lv_obj_t *home_btn = lv_btn_create(settings_screen);
  lv_obj_align(home_btn, LV_ALIGN_CENTER, 0, 120); // Align button
  lv_obj_t *home_btn_label = lv_label_create(home_btn);
  lv_label_set_text(home_btn_label, "Home"); // Set button text
  lv_obj_add_event_cb(home_btn, home_btn_event_handler, LV_EVENT_CLICKED, NULL); // Add event handler
}
  • The Settings Screen includes:

    • A label to show the target temperature.
    • A slider to adjust the target temperature.
    • A button to return to the Home Screen.

Adding Event Handlers for Screen Switching and Slider

To manage the screen transitions and slider changes, we define three event handlers:

  1. Switching to the Settings Screen when the "Settings" button is clicked.
  2. Switching back to the Home Screen when the "Home" button is clicked.
  3. Updating the Target Temperature when the slider is moved.
// Button event to switch to settings screen
void settigns_btn_event_handler(lv_event_t * e) {
  lv_scr_load(settings_screen); // Load the settings screen
}

// Button event to switch to home screen
void home_btn_event_handler(lv_event_t * e) {
  lv_scr_load(home_screen); // Load the home screen
}

// Slider event to update target temperature
void slider_event_handler(lv_event_t * e) {
  lv_obj_t * slider = lv_event_get_target(e);
  target_temp = lv_slider_get_value(slider); // Get the slider value
  lv_label_set_text_fmt(setpoint_label, "Target Temp: %d C", target_temp); // Update the label
}

Initialize the UI and Load the Home Screen

We initialize LVGL, set up a theme, and load the Home Screen by default.

void ui_init()
{
  // Set the theme
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);

  // Create both screens
  ui_create_home_screen();
  ui_create_settings_screen();

  // Load the home screen
  lv_disp_load_scr(home_screen);
}
  • We set a theme for the application (with a blue palette for the primary color and red for the secondary color).
  • Both screens are created when the UI initializes.
  • The Home Screen is loaded by default.

Setup LVGL and ESP32 Loop

Finally, we set up the setup() and loop() functions for the Arduino to manage the LVGL tasks.

void setup() {
  lv_init(); // Initialize LVGL
  // Initialize your display driver here...

  ui_init(); // Initialize the UI and load the screens
}

void loop() {
  lv_task_handler(); // Handle LVGL tasks
  delay(5); // Small delay for task handling
}

Complete Code

This code was tested on ESP32 with a 2.8 inch TFT display based on ILI9341 and XPT2046 touch screen. Please refers to this repository for hardware connection and to access to the whole project files.

#include <Arduino.h>

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

// Define UI Screens-views
lv_obj_t *home_screen;
lv_obj_t *settings_screen;

//Define UI Widgets
lv_obj_t * setpoint_label;
// Variables for the thermostat
int current_temp = 22;
int target_temp = 24;

// Local functions to create screens-views
void ui_create_home_screen();
void ui_create_settings_screen();

// Event Handlers
void settigns_btn_event_handler(lv_event_t * e);
void home_btn_event_handler(lv_event_t * e);
void slider_event_handler(lv_event_t * e);

void ui_init(void);
/*Change to your screen resolution*/
#define SCREEN_ROTATION 0 // set the screen rotation

/*Change to your screen resolution*/
#if (SCREEN_ROTATION == 1) || (SCREEN_ROTATION == 3)
static const uint16_t screenWidth = 320; // rotation 1 or 3
static const uint16_t screenHeight = 240;
#else
static const uint16_t screenWidth = 240; // rotation 0 or 2
static const uint16_t screenHeight = 320;
#endif

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[screenWidth * screenHeight / 10];

TFT_eSPI tft = TFT_eSPI(screenWidth, screenHeight); /* TFT instance */

#if LV_USE_LOG != 0
/* Serial debugging */
void my_print(const char *buf)
{
  Serial.printf(buf);
  Serial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);

  tft.startWrite();
  tft.setAddrWindow(area->x1, area->y1, w, h);
  tft.pushColors((uint16_t *)&color_p->full, w * h, true);
  tft.endWrite();

  lv_disp_flush_ready(disp);
}

/*Read the touchpad*/
void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
  uint16_t touchX = 0, touchY = 0;

  bool touched = tft.getTouch(&touchX, &touchY, 600);

  if (!touched)
  {
    data->state = LV_INDEV_STATE_REL;
  }
  else
  {
    data->state = LV_INDEV_STATE_PR;

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

    Serial.print("Data x ");
    Serial.println(touchX);

    Serial.print("Data y ");
    Serial.println(touchY);
  }
}

void setup()
{
  Serial.begin(115200); /* prepare for possible serial debug */

  String LVGL_Arduino = "Hello Arduino! ";
  LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

  Serial.println(LVGL_Arduino);
  Serial.println("I am LVGL_Arduino - MCiauTech");

  lv_init();

#if LV_USE_LOG != 0
  lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif

  tft.begin();        /* TFT init */
  tft.setRotation(0); /* Landscape orientation, flipped */
  /* TFT Touch Calibration Values */
  uint16_t calData[5] = {367, 3320, 583, 3264, 6};

  /* Calibrating TFT Touch */
  tft.setTouch(calData);

  lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * screenHeight / 10);

  /*Initialize the display*/
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  /*Change the following line to your display resolution*/
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);

  /*Initialize the (dummy) input device driver*/
  static lv_indev_drv_t indev_drv;
  lv_indev_drv_init(&indev_drv);
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  indev_drv.read_cb = my_touchpad_read;
  lv_indev_drv_register(&indev_drv);
  ui_init();
  Serial.println("Setup done");
}

void loop()
{
  lv_timer_handler(); /* let the GUI do its work */
  delay(5);
}

void ui_init()
{
  //setting theme
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);
  //create home screen
  ui_create_home_screen();
  //create settings screen
  ui_create_settings_screen();
  // load (show) home screen
  lv_disp_load_scr(home_screen);
}

void ui_create_home_screen()
{
  home_screen = lv_obj_create(NULL);

  lv_obj_t *temp_label = lv_label_create(home_screen);
  lv_label_set_text_fmt(temp_label, "Current Temp: %d C", current_temp);
  lv_obj_align(temp_label, LV_ALIGN_CENTER, 0, -50);

  lv_obj_t *settings_btn = lv_btn_create(home_screen);
  lv_obj_align(settings_btn, LV_ALIGN_CENTER, 0, 50);
  lv_obj_t *settings_btn_label = lv_label_create(settings_btn);
  lv_label_set_text(settings_btn_label, "Settings");
  lv_obj_add_event_cb(settings_btn, settigns_btn_event_handler, LV_EVENT_CLICKED, NULL);
}

void ui_create_settings_screen()
{
  settings_screen = lv_obj_create(NULL);

  setpoint_label = lv_label_create(settings_screen);
  lv_label_set_text_fmt(setpoint_label, "Set point: %d C", target_temp);
  lv_obj_align(setpoint_label, LV_ALIGN_CENTER, 0, -50);
  
  lv_obj_t * slider = lv_slider_create(settings_screen);
  lv_slider_set_range(slider, 16, 30);
  lv_slider_set_value(slider, target_temp, LV_ANIM_OFF);
  lv_obj_align(slider, LV_ALIGN_CENTER, 0, 50);
  lv_obj_set_width(slider, 180);
  lv_obj_add_event_cb(slider, slider_event_handler, LV_EVENT_VALUE_CHANGED, NULL);

  lv_obj_t *home_btn = lv_btn_create(settings_screen);
  lv_obj_align(home_btn, LV_ALIGN_CENTER, 0, 120);
  lv_obj_t *home_btn_label = lv_label_create(home_btn);
  lv_label_set_text(home_btn_label, "Home");
  lv_obj_add_event_cb(home_btn, home_btn_event_handler, LV_EVENT_CLICKED, NULL);
}

// Button event to switch settings screen
void settigns_btn_event_handler(lv_event_t * e) {
  lv_scr_load(settings_screen);
}

// Button event to switch home screen
void home_btn_event_handler(lv_event_t * e) {
  lv_scr_load(home_screen);
}

// Slider event to update target temperature
void slider_event_handler(lv_event_t * e) {
  lv_obj_t * slider = lv_event_get_target(e);
  target_temp = lv_slider_get_value(slider);
  lv_label_set_text_fmt(setpoint_label, "Set point: %d C", target_temp);
}

Conclusion

In this tutorial, we explored how to manage multiple screens effectively using LVGL on the ESP32, specifically for a thermostat application. We demonstrated how to create and switch between screens by pre-creating them during the initialization phase and handling user interactions with event-driven callbacks.

We discussed two approaches to managing screens: creating and destroying screens on demand and pre-creating screens. The latter approach, which we implemented here, is particularly suitable for embedded systems like the ESP32, as it ensures smooth and quick transitions between screens without delays caused by reloading UI elements. Although this approach consumes more memory, it offers faster screen switching and a more responsive user experience.

Feel free to use this approach as a template for managing multiple screens in your projects, whether you're building a simple thermostat, a more complex home automation system, or any other IoT interface that requires efficient screen management.


Profile picture

Written by Marco Ciau who is apassionate about providing solutions by using software. I thoroughly enjoy learning new things and am always eager to embrace new challenges. You can follow me on Twitter.