T-Watch. Mixer les librairies LVGL et TFT_eSPI dans un même projet ESP32 • Domotique et objets connectés à faire soi-même

La librairie LilyGoWatch intègre les libraries TFT_eSPI et LVGL pour construire l’affichage de l’application ESP32. La librairie TFT_eSPI est hyper simple et rapide à prendre en main, idéal pour faire un prototype. LVGL est une librairie professionnelle de haut niveau qui demande plus de temps d’apprentissage. Le rendu est juste hallucinant pour un projet bricolé soi-même.

La librairie TFT_eSPI est super simple d’utilisation et rapide à prendre en main idéal pour afficher du texte, des formes géomètriques simples. La librairie LVGL est une librairie de très haut niveau qui permet de construire des écrans complexes avec des animations, listes défilantes…  destinée à des applications industrielles.

Le code source a été testé sur la T-Watch Touch 2019

Ainsi que sur la T-Watch 2020

T-Watch et cartes d’extension

Quelques articles à lire avant de commencer votre projet T-Watch

Si vous débutez le développement de votre application pour votre T-Watch, voici quelques articles pour débuter

Activer la librairie LVGL dans votre projet Arduino ou PlatformIO pour T-Watch

Contrairement à la librairie TFT_eSPI, la librairie LVGL doit être activée manuellement au démarrage du projet en ajoutant la constante LILYGO_WATCH_LVGL avant de déclarer la librairie LilyGoWatch.

Dans un projet développé à l’aide de l’IDE Arduino. Dé-commenter le modèle de T-Watch utilisé

//#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  

Un exemple de fichier platformio.ini en utilisant l’option build_flags

[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

Initialiser la librairie LVGL

Avant de pouvoir accéder à l’API (fonctions) de la librairie LVGL, il faut l’initialiser en appelant la méthode ttgo->beginlvgl() dans le setup().

Voici le code minimum à exécuter avant de pouvoir utiliser l’écran de la T-Watch.

void setup() {
  ttgo = TTGOClass::getWatch();
  ttgo->begin();
  ttgo->lvgl_begin();
  ttgo->openBL();
}

Exemple 1 : page principale créée avec LVGL

Dans ce premier exemple, nous allons apprendre comment créer un projet dont la page principale est construite avec la librairie LVGL. La seconde page est construit avec la librairie TFT_eSPI.

Créer un nouveau projet avec l’IDE Arduino ou PlatformIO et collez le code suivant. Vous pouvez également récupérer le code source directement sur GitHub.

sueyd1wlitttxpo7iz9p-5448372

/* 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

// Arduino IDE - dé-commenter pour activer LVGL
//#define LILYGO_WATCH_LVGL

/* PlatformIO -> Select your watch in platformio.ini file */
#include 
#include 

QueueHandle_t g_event_queue_handle = nullptr;

// Enumère les événements de la queue
enum {
  Q_EVENT_DISPLAY_TFT,
};

// Déclare l'image de fond
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);

  // Créé une file d'événement que l'on va utiliser pour déclencher l'affichage de l'écran TFT_eSPI
  g_event_queue_handle = xQueueCreate(20, sizeof(uint8_t));

  ttgo = TTGOClass::getWatch();
  ttgo->begin();
  ttgo->lvgl_begin();

  // Allume le rétro-éclairage
  ttgo->openBL();

  // et affiche l'écran principal
  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:{
        // cache l'écran LVGL
        hideLVGLpage(true);
        //  affiche l'écran eSPI et attend que l'utilisateur touche l'écran
        // Dans l'écran eSPI, le superviseur LVGL n'est pas exécuté
        while ( openTFT() ) {}
        // Affiche la page d'accueil LVGL
        hideLVGLpage(false);
      }
      break;
      default:
        break;
    }
  }  
  // Superviseur LVGL
  lv_task_handler();
}

// Créé un écran avec la librairie LVGL
void createLVGLPage(){
  // Conteneur qui contient tous les élements affiché. Permet d'afficher ou masquer plus facilement la page
  lvglpage = lv_cont_create( lv_scr_act(), NULL );
  lv_obj_set_width( lvglpage, lv_disp_get_hor_res( NULL )  );  // résolution horizontale
  lv_obj_set_height( lvglpage, lv_disp_get_ver_res( NULL ) );  // résolution verticale

  // 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);

  // 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 
  // Affiche un message défilant de bienvenue
  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);

  //  libellé du bouton
  lv_obj_t * label;
  label = lv_label_create(btn1, NULL);
  lv_label_set_text(label, "Go to TFT_eSPI");
}

//  Déclencheur du bouton pour passer d'un écran LVGL à TFT_eSPI
static void event_handler(lv_obj_t * obj, lv_event_t event){
  // 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 => send open TFT Screen");
    uint8_t data = Q_EVENT_DISPLAY_TFT;
    xQueueSend(g_event_queue_handle, &data, portMAX_DELAY);
  }  
}

// Masque / affiche la page LVGL
void hideLVGLpage(bool hide){
  lv_obj_set_hidden(lvglpage, hide);
}

// Créé un écran avec la librairie TFT_eSPI
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);

    // Attend que l'utilisateur touche l'écran pour sortir
    while (onAir) {

      if (!onAir) return false;
      
      int16_t x,y;

      if (ttgo->getTouch(x, y)) {
        while (ttgo->getTouch(x, y)) {}           // Attend que l'utilisateur relaâche l'écran
        Serial.println("User touch the screen");
        onAir = false;
      }     
    }  
} 

Un petit clic de démonstration

Explication du code

LVGL utilise un superviseur (handler) pour actualiser l’écran et détecter les actions de l’utilisateur sur l’écran (clic sur un bouton, défilement d’une liste…). Le plus facile est donc d’appeler le lv_task_handler() dans la boucle loop().

Le problème, c’est qu’il faut suspendre l’appel du superviseur le temps qu’on utilise l’écran construit avec la librairie TFT_eSPI.

Pour cela, le plus simple est d’utiliser le système d’événement de FreeRTOS. FreeRTOS est le système de base sur lequel Espressif a construit son SDK ESP-IDF pour ESP32.

Il suffit de créer un objet QueueHandle_t.

QueueHandle_t g_event_queue_handle = nullptr;

// Enumère les événements possibles
enum {
  Q_EVENT_DISPLAY_TFT,
};

puis d’initialiser la fil d’attente dans le setup()

g_event_queue_handle = xQueueCreate(20, sizeof(uint8_t));

Ensuite voici ce qui se passe :

  • Le superviseur du bouton event_handler() est appelé dès que l’utilisateur Touche l’écran
  • Le superviseur envoi dans la pile un nouveau message Q_EVENT_DISPLAY_TFT demandant l’affichage de l’écran TFT_eSPI à l’aide de la fonction xQueueSend de FreeRTOS.
  • Dès que le message est reçu dans la loop() par xQueueReceive()
    • On masque la page LVGL. Comme tous les éléments sont dans un conteneur, il suffit d’appeler la méthode lv_obj_set_hiden(nom_objet,false) pour la masquer
    • On appel la méthode openTFT() qui construit la librairie TFT_eSPI.
    • Une boucle while() bloque l’appel de lv_task_handler(), ce qui empêche LVGL de reconstruire l’écran
    • Ici on sort de la boucle while() en touchant l’écran mais on pourrait créer un bouton ou utiliser le bouton utilisateur de la T-Watch.
  • En sortant de la page TFT_eSPI, on ré-active l’affichage de la page LVGL en appelant la méthode lv_obj_set_hiden(nom_objet,true). Eventuellement, on peut forcer l’actualisation de l’affichage avec

oy85eupthmtye9zobc8a-8462957

Exemple 2, page principale construite avec TFT_eSPI

Voyons maintenant l’inverse. La page principale du projet est créé avec la librairie TFT_eSPI. La page secondaire avec LVGL.

Aucun superviseur n’est nécessaire puisqu’il suffit d’utiliser la méthode ttgo->getTouch() pour détecter une action sur l’écran dans la boucle loop(). La gestion des appels est donc bien plus simple et on n’aura pas besoin d’utiliser le gestionnaire de tâche xQueue de FreeRTOS.

On construit la page LVGL comme précédemment. Pour maintenir la page ouverte et la quitter, l’astuce consiste à changer l’état d’une variable. Tant que celle-ci n’est pas vrai, on appel régulièrement le superviseur lv_task_handler(). Ici par exemple, il est appelé toutes les 20ms.

while (!KeyPressed) {lv_task_handler(); delay(20);} 

Il est préférable de détruire la page LVGL lorsqu’on retourne à la page d’accueil pour libérer les ressources.

lv_obj_del(lvglpage);

L’affichage de l’écran principal est géré dans la boucle principale loop(). En sortant de la page LVGL, il suffit de renvoyer un état pour déclencher la re-construction de l’écran principal. On pourra utiliser la même stratégie pour récupérer une valeur d’une page de configuration, par exemple un mot passe…

void loop() {
  int16_t x, y;
  if (ttgo->getTouch(x, y)) {
    while (ttgo->getTouch(x, y)) {} // wait for user to release
    if ( gotoLVGLPage() ) buildTFTPage();
  }  
}

Voici un petit clic de démo

Et le code Arduino complet de l’exemple que vous pouvez également retrouver sur GitHub

sueyd1wlitttxpo7iz9p-5448372

/* 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 
#include 

// 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();

  // Allume le rétro-éclairage
  ttgo->openBL();
  // créé la page principale
  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);
  // Toucher l'écran pour sortir
  tft->drawString("Touch screen to open LVGL page", 0,20);
}

// Créé un écran avec la librairie LVGL
bool gotoLVGLPage(){
  KeyPressed = false;
  // Container that contain all displayed elements. Makes it easier to show or hide the page 
  // Conteneur qui contient tous les élements affiché. Permet d'afficher ou masquer plus facilement la page
  lvglpage = lv_cont_create( lv_scr_act(), NULL );
  lv_obj_set_width( lvglpage, lv_disp_get_hor_res( NULL )  );  // Horizontal resolution | résolution horizontale
  lv_obj_set_height( lvglpage, lv_disp_get_ver_res( NULL ) );  // Vertical resolution | résolution verticale

  // 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);

  // 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);
  
  // Affiche un message défilant de bienvenue
  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);

  // libellé du bouton
  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;
}

// Déclencheur du bouton retourner à l'écran d'accueil
static void event_handler(lv_obj_t * obj, lv_event_t event){
  if (event == LV_EVENT_CLICKED) {
    Serial.println("event_handler => return main page");
    KeyPressed = true;
  }  
}

Remarque concernant les librairies TTGO.h et LilyGoWatch.h

Vous trouverez de nombreux tutoriels pour les T-Watch sur GitHub et sur d’autres blogs faisant référence à la librairie TTGO.h. C’est tout simplement la première version de la librairie développée par LilyGo.

Le dépôt, toujours disponible sur GitHub, est déprécié.

Vous pouvez toujours vous inspirer des exemples mais il faudra faire des adaptations du code

La librairie LilyGoWatch prend la suite. Certaines fonctions ne sont plus disponibles ou les appels sont différents

Par exemple, l’API de la librairie TFT_eSPI était accessible depuis la classe eTFT.

TTGOClass *watch = TTGOClass::getWatch();
TFT_eSPI *tft = watch->eTFT;

Maintenant, c’est la classe tft que l’on doit appeler

TTGOClass *watch = TTGOClass::getWatch();
TFT_eSPI *tft = watch->tft;

Certains projets intègrent directement un version modifiée de la librairie TTGO.h dans le dossier lib (projet PlatformIO). Si vous avez pris l’habitude d’utiliser la nouvelle version, vous risquez de perdre pas mal de temps à chercher vos erreurs et les méthodes…

Mises à jour

20/11/2020 Publication de l’article

English Version

Avez-vous aimé cet article ?