The LilyGoWatch library integrates the TFT_eSPI and LVGL libraries to build the display of the ESP32 application. The TFT_eSPI library is super simple and quick to learn, ideal for making a prototype. LVGL is a high level professional bookstore that takes more time to learn. The rendering is just mind blowing for a DIY project.
The TFT_eSPI library is super easy to use and quick to learn, ideal for displaying text, simple geometric shapes. The LVGL library is a very high level library that allows you to build complex screens with animations, scrolling lists, etc. intended for industrial applications.
The source code has been tested on the T-Watch Touch 2019
As well as on the T-Watch 2020
Some articles to read before starting your T-Watch project
If you are new to developing your app for your T-Watch, here are some articles to get you started
Activate the LVGL library in your Arduino or PlatformIO project for T-Watch
Unlike the TFT_eSPI library, the LVGL library must be activated manually when starting the project by adding the LILYGO_WATCH_LVGL constant before declaring the LilyGoWatch library.
In a project developed using the Arduino IDE. Comment out the T-Watch model used
//#define LILYGO_WATCH_2019_WITH_TOUCH
//#define LILYGO_WATCH_2019_NO_TOUCH
//#define LILYGO_WATCH_BLOCK
#define LILYGO_WATCH_2020_V1
#define LILYGO_WATCH_LVGL
#include <LilyGoWatch.h>
A sample platformio.ini file using the build_flags option
[env:ttgo-t-watch]
platform = espressif32
board = ttgo-t-watch
framework = arduino
build_flags =
-D LILYGO_WATCH_2019_WITH_TOUCH=1
;-D LILYGO_WATCH_2019_NO_TOUCH=1
;-D LILYGO_WATCH_BLOCK=1
;-D LILYGO_WATCH_2020_V1=1
;-D LILYGO_WATCH_LVGL=1
; Important, activate LVGL support
-D LILYGO_WATCH_LVGL=1
lib_deps =
TTGO TWatch Library
upload_speed = 2000000
monitor_speed = 115200
Initialize the LVGL library
Before being able to access the API (functions) of the LVGL library, it must be initialized by calling the ttgo->beginlvgl() method in the setup().
Here is the minimum code to run before you can use the T-Watch screen.
void setup() {
ttgo = TTGOClass::getWatch();
ttgo->begin();
ttgo->lvgl_begin();
ttgo->openBL();
}
Example 1: main page created with LVGL
In this first example, we will learn how to create a project whose main page is built with the LVGL library. The second page is built with the TFT_eSPI library.
Create a new project with the Arduino or PlatformIO IDE and paste the following code. You can also grab the source code directly from GitHub.
/* Arduino IDE - uncomment your watch */
//#define LILYGO_WATCH_2019_WITH_TOUCH
//#define LILYGO_WATCH_2019_NO_TOUCH
//#define LILYGO_WATCH_BLOCK
//#define LILYGO_WATCH_2020_V1
// Arduino IDE - uncomment to activate LVGL library
//#define LILYGO_WATCH_LVGL
/* PlatformIO -> Select your watch in platformio.ini file */
#include <Arduino.h>
#include <LilyGoWatch.h>
QueueHandle_t g_event_queue_handle = nullptr;
// Enum Queue events
enum {
Q_EVENT_DISPLAY_TFT,
};
// Declare background image
LV_IMG_DECLARE(WALLPAPER_1_IMG);
/**************************/
/* Static variables */
/**************************/
TTGOClass *ttgo = nullptr;
static bool onAir = true;
static lv_obj_t *lvglpage = NULL;
/**************************/
/* STATIC PROTOTYPES */
/**************************/
void createLVGLPage();
static void event_handler(lv_obj_t * obj, lv_event_t event);
void hideLVGLpage(bool hide);
bool openTFT();
/**************************/
void setup() {
Serial.begin(115200);
// Create an event queue that we will use to trigger the display of the TFT_eSPI screen
g_event_queue_handle = xQueueCreate(20, sizeof(uint8_t));
ttgo = TTGOClass::getWatch();
// Initialize the hardware
ttgo->begin();
ttgo->lvgl_begin();
// Turn on the backlight
ttgo->openBL();
// and create main screen
createLVGLPage();
}
void loop() {
uint8_t data;
if (xQueueReceive(g_event_queue_handle, &data, 5 / portTICK_RATE_MS) == pdPASS) {
switch (data) {
case Q_EVENT_DISPLAY_TFT:{
//Hide LVGL screen
hideLVGLpage(true);
// Display TFT_eSPI screen and wait user touch the screen
// Inside the eSPI screen, LVGL handler in not executed
while ( openTFT() ) {}
// Show LVGL main page | Affiche la page d'accueil LVGL
hideLVGLpage(false);
}
break;
default:
break;
}
}
// LGVL handler | Superviseur LVGL
lv_task_handler();
}
// Create a screen with LVGL library
void createLVGLPage(){
// Container that contain all displayed elements. Makes it easier to show or hide the page
lvglpage = lv_cont_create( lv_scr_act(), NULL );
lv_obj_set_width( lvglpage, lv_disp_get_hor_res( NULL ) ); // Horizontal resolution
lv_obj_set_height( lvglpage, lv_disp_get_ver_res( NULL ) ); // Vertical resolution
// Background Image | Image de fond
lv_obj_t * img1 = lv_img_create(lvglpage, NULL);
lv_img_set_src(img1, &WALLPAPER_1_IMG);
lv_obj_align(img1, NULL, LV_ALIGN_CENTER, 0, 0);
// Button in center of the screen | Bouton au centre de l'écran
lv_obj_t * btn1 = lv_btn_create(lvglpage, NULL);
lv_obj_set_event_cb(btn1, event_handler);
lv_obj_align(btn1, NULL, LV_ALIGN_CENTER, 0, 0);
// Display a circuar scrolling welcome message
lv_obj_t * welcomemessage;
welcomemessage = lv_label_create(lvglpage, NULL);
lv_label_set_long_mode(welcomemessage, LV_LABEL_LONG_SROLL_CIRC); /*Circular scroll*/
lv_obj_set_width(welcomemessage, lv_disp_get_hor_res( NULL ));
lv_label_set_text(welcomemessage, "Welcome on LVGL Demo Screen for TTGO T-Wach");
lv_obj_align(welcomemessage, btn1, LV_ALIGN_CENTER, 0, -60);
// Button label | libellé du bouton
lv_obj_t * label;
label = lv_label_create(btn1, NULL);
lv_label_set_text(label, "Go to TFT_eSPI");
}
// Switch screen button handler | Déclencheur du bouton pour passer d'un écran LVGL à TFT_eSPI
static void event_handler(lv_obj_t * obj, lv_event_t event){
// Important !! always test event to avoid multiple signals
l'affichage de plusieurs écrans TFT_eSPI
if (event == LV_EVENT_CLICKED) {
Serial.println("event_handler => send open TFT Screen");
uint8_t data = Q_EVENT_DISPLAY_TFT;
xQueueSend(g_event_queue_handle, &data, portMAX_DELAY);
}
}
// Show / hide LVGL page L
void hideLVGLpage(bool hide){
lv_obj_set_hidden(lvglpage, hide);
}
// Create a screen with TFT_eSPI library
bool openTFT(){
Serial.println("Display TFT_eSPI screen");
onAir = true;
TTGOClass *ttgo = TTGOClass::getWatch();
TFT_eSPI *tft = ttgo->tft;
tft->fillScreen(TFT_BLACK);
tft->setTextSize(2);
tft->setTextColor(TFT_WHITE);
tft->drawString("TFT_eSPI Screen", 0,0);
// Toucher l'écran pour sortir
tft->drawString("Touch screen to exit", 0,20);
// Wait user touch the screen to exit
while (onAir) {
if (!onAir) return false;
int16_t x,y;
if (ttgo->getTouch(x, y)) {
while (ttgo->getTouch(x, y)) {} // wait for user to release
Serial.println("User touch the screen");
onAir = false;
}
}
}
A small click of demonstration
Explanation of the code
LVGL uses a supervisor (handler) to update the screen and detect user actions on the screen (click on a button, scroll through a list, etc.). The easiest way is to call the lv_task_handler() in the loop().
The problem is that you have to suspend the supervisor call while you are using the screen built with the TFT_eSPI library.
The easiest way to do this is to use the FreeRTOS event system. FreeRTOS is the base system on which Espressif built its ESP-IDF SDK for ESP32.
Just create a QueueHandle_t object.
QueueHandle_t g_event_queue_handle = nullptr;
enum {
Q_EVENT_DISPLAY_TFT,
};
then to initialize the waiting line in the setup()
g_event_queue_handle = xQueueCreate(20, sizeof(uint8_t));
Then here is what happens:
- The supervisor of the event_handler() button is called as soon as the user touches the screen
- The supervisor sends a new Q_EVENT_DISPLAY_TFT message to the stack requesting the display of the TFT_eSPI screen using the xQueueSend function of FreeRTOS.
- As soon as the message is received in the loop() by xQueueReceive()
- We hide the LVGL page. As all the elements are in a container, just call the lv_obj_set_hiden(object_name, false) method to hide it
- We call the openTFT() method which builds the TFT_eSPI library.
- A while() loop blocks the call to lv_task_handler(), which prevents LVGL from rebuilding the screen
- Here we exit the while() loop by touching the screen but we could create a button or use the T-Watch user button.
- When exiting the TFT_eSPI page, the display of the LVGL page is re-enabled by calling the lv_obj_set_hiden(name_object, true) method. Optionally, you can force the updating of the display with
Example 2, main page built with TFT_eSPI
Now let’s see the reverse. The main page of the project is created with the TFT_eSPI library. The secondary page with LVGL.
No supervisor is necessary since it suffices to use the ttgo->getTouch() method to detect an action on the screen in the loop(). Call management is therefore much simpler and we will not need to use the FreeRTOS xQueue task manager.
We build the LVGL page as before. The trick to keeping the page open and exiting it is to change the state of a variable. As long as this is not true, we regularly call the supervisor lv_task_handler(). Here for example, it is called every 20ms.
while (!KeyPressed) {lv_task_handler(); delay(20);}
It is better to destroy the LVGL page when returning to the home page to free up resources.
lv_obj_del(lvglpage);
The display of the main screen is managed in the main loop(). When exiting the LVGL page, it suffices to return a state to trigger the re-construction of the main screen. We can use the same strategy to retrieve a value from a configuration page, for example a password …
void loop() {
int16_t x, y;
if (ttgo->getTouch(x, y)) {
while (ttgo->getTouch(x, y)) {} // wait for user to release
if ( gotoLVGLPage() ) buildTFTPage();
}
}
Here is a small demo click
And the full Arduino code of the example that you can also find on GitHub
/* Arduino IDE - dé-commenter votre T-Watch */
//#define LILYGO_WATCH_2019_WITH_TOUCH
//#define LILYGO_WATCH_2019_NO_TOUCH
//#define LILYGO_WATCH_BLOCK
//#define LILYGO_WATCH_2020_V1
#define LILYGO_WATCH_LVGL
/* PlatformIO -> Select your watch in platformio.ini file */
#include <Arduino.h>
#include <LilyGoWatch.h>
// Déclare l'image de fond
LV_IMG_DECLARE(WALLPAPER_1_IMG);
/**************************/
/* Static variables */
/**************************/
TTGOClass *ttgo = nullptr;
static lv_obj_t *lvglpage = NULL;
bool KeyPressed = false;
/**************************/
/* STATIC PROTOTYPES */
/**************************/
bool gotoLVGLPage();
static void event_handler(lv_obj_t * obj, lv_event_t event);
void buildTFTPage();
/**************************/
void setup() {
Serial.begin(115200);
ttgo = TTGOClass::getWatch();
// Initialize the hardware
ttgo->begin();
ttgo->lvgl_begin();
// Turn on the backlight
ttgo->openBL();
// Build landing page
buildTFTPage();
}
void loop() {
int16_t x, y;
if (ttgo->getTouch(x, y)) {
while (ttgo->getTouch(x, y)) {} // wait for user to release
if ( gotoLVGLPage() ) buildTFTPage();
}
}
void buildTFTPage(){
TFT_eSPI *tft = ttgo->tft;
tft->fillScreen(TFT_BLACK);
tft->setTextSize(2);
tft->setTextColor(TFT_WHITE);
tft->drawString("TFT_eSPI Screen", 0,0);
tft->drawString("Touch screen to open LVGL page", 0,20);
}
// Create a screen with LVGL library
bool gotoLVGLPage(){
KeyPressed = false;
// Container that contain all displayed elements. Makes it easier to show or hide the page
lvglpage = lv_cont_create( lv_scr_act(), NULL );
lv_obj_set_width( lvglpage, lv_disp_get_hor_res( NULL ) ); // Horizontal resolution
lv_obj_set_height( lvglpage, lv_disp_get_ver_res( NULL ) ); // Vertical resolution
// Background Image
lv_obj_t * img1 = lv_img_create(lvglpage, NULL);
lv_img_set_src(img1, &WALLPAPER_1_IMG);
lv_obj_align(img1, NULL, LV_ALIGN_CENTER, 0, 0);
// Button in center of the screen
lv_obj_t * btn1 = lv_btn_create(lvglpage, NULL);
lv_obj_set_event_cb(btn1, event_handler);
lv_obj_align(btn1, NULL, LV_ALIGN_CENTER, 0, 0);
// Display a circular scrolling welcome message
lv_obj_t * welcomemessage;
welcomemessage = lv_label_create(lvglpage, NULL);
lv_label_set_long_mode(welcomemessage, LV_LABEL_LONG_SROLL_CIRC); /*Circular scroll*/
lv_obj_set_width(welcomemessage, lv_disp_get_hor_res( NULL ));
lv_label_set_text(welcomemessage, "Welcome on LVGL Demo Screen for TTGO T-Wach");
lv_obj_align(welcomemessage, btn1, LV_ALIGN_CENTER, 0, -60);
// Button label
lv_obj_t * label;
label = lv_label_create(btn1, NULL);
lv_label_set_text(label, "Exit");
while (!KeyPressed) {lv_task_handler(); delay(20);} // Wait for touch
Serial.print("Exit LVGL page");
lv_obj_del(lvglpage);
return true;
}
// Switch screen button handler
static void event_handler(lv_obj_t * obj, lv_event_t event){
// Important !! always test event to avoid multiple signals
// Il faut toujours tester l'évènement sinon plusieurs signaux sont envoyés dans la queue ce qui entraîne l'affichage de plusieurs écrans TFT_eSPI
if (event == LV_EVENT_CLICKED) {
Serial.println("event_handler => return main page");
KeyPressed = true;
}
}
Note concerning the TTGO.h and LilyGoWatch.h libraries
You will find many tutorials for T-Watch on GitHub and on other blogs referring to the TTGO.h library. It is quite simply the first version of the library developed by LilyGo.
The repository, still available on GitHub, is deprecated.
You can always take inspiration from the examples but you will have to adapt the code
The LilyGoWatch bookstore takes over. Some functions are no longer available or the calls are different
For example, the API of the TFT_eSPI library was accessible from the eTFT class.
TTGOClass *watch = TTGOClass::getWatch();
TFT_eSPI *tft = watch->eTFT;
Now it’s the tft class that we have to call
TTGOClass *watch = TTGOClass::getWatch();
TFT_eSPI *tft = watch->tft;
Some projects directly integrate a modified version of the TTGO.h library in the lib folder (PlatformIO project). If you’ve gotten into the habit of using the new version, you might waste a lot of time looking for your mistakes and methods …
Updates
2020/11/20 Publication of the article