Pantallas con LVGL en ESPHome

Pantallas con LVGL en ESPHome

Ha llegado el momento de pasar al siguiente nivel con nuestros dispositivos DIY y empezar a potenciar las pantallas con LVGL en ESPHome.

¿Qué es LVGL y para qué sirve?

Previamente ya hemos personalizado otras pantallas utilizando los componentes tradicionales de ESPHome (como la Lilygo T-Display S3 Long, el M5Stack Dial o el CrowPanel E-paper). En todos estos ejercicios he aprendido mucho sobre su funcionamiento y he creado dispositivos funcionales con pantallas muy baratas.

Sin embargo, también me he encontrado con algunas limitaciones en la navegabilidad. Por ejemplo, a la hora de tener controles táctiles o navegar entre pantallas. La verdad es que nunca me ha supuesto un problema y lo he podido solucionar con un poco de creatividad.

No obstante, cada vez caen en nuestras manos dispositivos mas potentes con pantallas de calidad y me parece un desperdicio no explotar todo su potencial. Aquí es donde aparece LVGL en escena, permitiendo crear interfaces avanzadas dignas de cualquier aplicación.

LVGL («Light and Versatile Graphics Library») es una biblioteca gráfica de código abierto diseñada para crear interfaces gráficas en dispositivos con recursos limitados, como nuestras queridas placas ESP32. Aunque esta biblioteca se encuentra programada en ‘C’, hace ya unas cuantas versiones que podemos utilizarla en ESPHome, a través del componente correspondiente.

Con este objetivo en mente, vamos a aprender a crear pantallas con LVGL en ESPHome!

Requisitos previos

Para programar pantallas con LVGL en ESPHome previamente necesitas:

  • Alguna pantalla soportada con ESPHome, preferentemente con PSRAM y táctil (por ejemplo, un SenseCAP Indicator o un Lilygo T-Display S3 Long).
  • Un cable USB-C para alimentar la placa de DATOS (con un cable de carga no vas a poder instalar el software).
  • Haber instalado ESPHome en Home Assistant.

*Algún precio puede haber cambiado desde la ultima revision

Principios a tener en cuenta

Si, como yo, ya habías experimentado con otras pantallas y componentes, o simplemente quieres descubrir cómo crear pantallas con LVGL en ESPHome estas ideas clave te serán de ayuda:

  • Para usar LVGL con ESPHome, necesitarás una placa ESP32 o un RP2040. Se recomienda que tengan PSRAM (sobretodo para pantallas a color grandes), aunque no es un requisito estricto.
  • La configuración de cada pantalla en ESPHome es diferente (te sugiero que te guíes por algún proyecto documentado). En esta entrada voy a dar por hecho que ya tienes tu pantalla integrada, ya que a partir de ahí LVGL funciona exactamente igual en cualquier dispositivo.
  • LVGL es una librería muy personalizable, pero también más compleja de configurar. Si tienes una pantalla con poca memoria, no táctil o simplemente quieres desplegar algunos datos, te recomiendo que sigas una configuración «típica».
  • El uso de LVGL es incompatible con el uso de ‘lambdas’. Por lo tanto, en tu componente ‘display’ debes elegir entre uno u otro.
  • El componente display debe incluir la configuración ‘auto_clear_enabled: false’ y (en la mayoría de pantallas) ‘update_interval: never’.
  • Una interfaz creada con LVGL se organiza una o varias páginas, que contienen uno o varios ‘widgets’ u «objetos». Cada uno de estos widgets representa un elemento con el que puedes interactuar y en la documentación de ESPHome encontrarás un listado de los que puedes usar.
  • Puedes encontrar un montón de códigos de ejemplo en la documentación de ESPHome.

Teniendo en cuenta todo lo anterior (y la parte correspondiente a la configuración de tu placa y tu pantalla), este podría ser un código de partida:

display:
## Aquí iría la configuración del componente de tu pantalla ##

touchscreen:
  - platform: XXXXXX
    id: display_touchscreen
    ## Aquí iría el resto de la configuración del componente táctil de tu pantalla ##

lvgl:
  touchscreens: display_touchscreen
  buffer_size: 100%
  widgets:
    - label:
        align: CENTER
        text: 'Hello World!'

Creación de widgets

Esta es una de las partes más interesantes que ofrece LVGL. Como te decía, puedes incluir tantos ‘widgets’ u objetos interactivos como quieras. Además, un widget puede contener, a su vez, otros widgets.

Todos los widgets comparten algunas propiedades comunes (como la posición en ‘x’ e ‘y’, el tamaño en ‘width’ y ‘height’, o su ‘id). A partir de ahí cada tipo de widget tiene unas propiedades específicas y se configura de forma diferente. Además, ESPHome permite definir acciones, condiciones y desencadenantes para interactuar con los widgets (por ejemplo, la acción ‘lvgl.widget.show’ sirve para mostrar un widget oculto).

Vamos a empezar creando unos componentes básicos para un panel de control, como un texto (‘label’) y un botón (‘button’).

display:
## Aquí iría la configuración del componente de tu pantalla ##

touchscreen:
  - platform: XXXXXX
    id: display_touchscreen
    ## Aquí iría el resto de la configuración del componente táctil de tu pantalla ##

lvgl:
  touchscreens: display_touchscreen
  buffer_size: 100%
  widgets:
    - label:
        align: TOP_LEFT
        id: lbl_title
        text_font: montserrat_28
        text: "Aguacatec"
    - button:
        x: 50
        y: 50
        width: 70
        height: 30
        id: btn_id
        checkable: true
        widgets:
          - label:
              align: center
              text: "Light"

Repasa el código modificado y observa lo siguiente:

  • En este caso he incluido dos widgets, y el segundo (botón) contiene a su vez otro widget en su interior (texto).
  • En ambos casos les he indicado su posición. En el primer widget he utilizado el parámetro de alineación y en el segundo las coordenadas ‘x’ e ‘y’ de la pantalla. Algo que me gusta de LVGL es que sus elementos heredan las propiedades de sus contenedores. Por ejemplo, si un texto tiene alineación centrada, y está dentro de un botón, aparecerá en el centro del mismo (sin especificar coordenadas).

Uso de variables

Por supuesto, como en el resto de dispositivos que hemos creado anteriormente, vamos a poder utilizar nuestra información de Home Assistant para mostrar nuestros sensores y controlar nuestros dispositivos.

La diferencia respecto a como lo haciamos con la forma tradicional es que, por un lado, vamos a definir la estructura de la interfaz y, por otro, actualizaremos los valores mostrados mediante otros eventos utilizando la acción ‘lvgl.label.update’. Por ejemplo, voy a añadir dos widget tipo texto para mostrar el reloj y la temperatura.

El primero se actualizará una vez cada 60 segundos y editará el valor por defecto de la etiqueta correspondiente. El segundo hará lo mismo, cada vez que Home Assistant le reporte la temperatura.

display:
## Aquí iría la configuración del componente de tu pantalla ##

touchscreen:
  - platform: XXXXXX
    id: display_touchscreen
    ## Aquí iría el resto de la configuración del componente táctil de tu pantalla ##

lvgl:
  touchscreens: display_touchscreen
  buffer_size: 100%
  widgets:
    - label:
        align: TOP_LEFT
        id: lbl_clock
        text_font: montserrat_28
        text: "00:00"
    - label:
        align: TOP_LEFT
        id: lbl_weather
        text_font: montserrat_28
        text: "25C"
        pad_top: 30
    - button:
        x: 100
        y: 100
        width: 70
        height: 30
        id: btn_id
        checkable: true
        widgets:
          - label:
              align: center
              text: "Light"

time:
  - platform: homeassistant
    id: esptime

interval:
  - interval: 60s
    then:
      - lvgl.label.update:
          id: lbl_clock
          text: !lambda 'return id(esptime).now().strftime("%H:%M");'
sensor:
  - platform: homeassistant
    id: sensor_temperature
    entity_id: sensor.openweathermap_temperature
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_weather
            text:
              format: "%.0fC"
              args:
                - id(sensor_temperature).state

Dando estilo a la interfaz

Una de las características de las pantallas con LVGL en ESPHome es que tienen un aspecto más profesional y sofisticado. Para ello, lógicamente tenemos que aprender a darles un poco de estilo.

De nuevo, puedes consultar la documentación para encontrar todas las opciones disponibles. No obstante, me parece destacable comentar lo siguiente:

  • Con el atributo ‘bg_color’ puedes personalizar el color de fondo (en formato hexadecimal o usando sus nombres estandarizados).
  • Con el atributo ‘default_font’ puedes indicar un tipo de fuente por defecto para la interfaz (indicando el nombre y el tamaño, o cargando la fuente como lo hemos hecho «siempre»).
  • Con el atributo ‘text_color’ puedes personalizar el color del texto.
  • Usando la acción ‘lvgl.style.update’ puedes cambiar cualquier aspecto estético al producirse un evento (como el encendido de una luz).
display:
## Aquí iría la configuración del componente de tu pantalla ##

touchscreen:
  - platform: XXXXXX
    id: display_touchscreen
    ## Aquí iría el resto de la configuración del componente táctil de tu pantalla ##

lvgl:
  touchscreens: display_touchscreen
  buffer_size: 100%
  bg_color: 0x062e20
  default_font: font_clock
  widgets:
    - label:
        align: TOP_LEFT
        id: lbl_clock
        text_font: montserrat_28
        text: "00:00"
    - label:
        align: TOP_LEFT
        id: lbl_weather
        text_font: montserrat_28
        text: "25C"
        pad_top: 30
    - button:
        x: 100
        y: 100
        width: 70
        height: 30
        id: btn_id
        checkable: true
        widgets:
          - label:
              align: center
              text: "Light"

time:
  - platform: homeassistant
    id: esptime

interval:
  - interval: 60s
    then:
      - lvgl.label.update:
          id: lbl_clock
          text: !lambda 'return id(esptime).now().strftime("%H:%M");'
sensor:
  - platform: homeassistant
    id: sensor_temperature
    entity_id: sensor.openweathermap_temperature
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_weather
            text:
              format: "%.0fC"
              args:
                - id(sensor_temperature).state

font:
  - file: "gfonts://Space Grotesk"
    id: font_clock
    size: 30

Algo muy interesante que puedes hacer cuando utilices elementos de forma recurrente (por ejemplo, un botón) es crear una plantilla de estilo y aplicarla a todos los elementos que quieras. De esta forma podrás editar todos a la vez muy fácilmente.

lvgl:
  touchscreens: ft5x06_touchscreen
  buffer_size: 100%
  style_definitions:
    - id: style_button
      width: 205
      height: 145
      bg_color: 0x021911
      border_color: 0x021911
  widgets:
    - obj:
        styles: style_button
        x: 30
        y: 155
    - obj:
        styles: style_button
        x: 245
        y: 155
    - obj:
        styles: style_button
        x: 30
        y: 315
    - obj:
        styles: style_button
        x: 245
        y: 315

Inclusión de imágenes e iconos

Por supuesto, también podemos incluir imágenes en nuestra interfaz usando el widget ‘image’. La configuración de la ruta de la imagen se realiza, como siempre, con el componente ‘image’. La única particularidad es que el único tipo de imagen soportado es ‘RGB565’ (si indicas otro no la cargará).

image:
  - file: https://img.icons8.com/?size=100&id=qA3w9Yp2vY7r&format=png&color=000000
    id: icon_weather
    resize: 70x70
    type: RGB565
    transparency: alpha_channel

lvgl:
  touchscreens: display_touchscreen
  buffer_size: 100%
  widgets:
    - image:
        align: TOP_RIGHT
        src: icon_weather
        id: widget_weather
        pad_top: 45
        pad_right: 20

También puedes usar iconos en tu interfaz de forma sencilla. En mi caso lo he hecho agregando la fuente Material Symbols de Google Fonts, y agregando los iconos que me interesan (puedes consultar el código -code point- correspondiente desde su web).

font:
  - file: "gfonts://Material+Symbols+Outlined"
    id: font_icons
    size: 50
    glyphs: [
      "\U0000e846", # thermometer
      "\U0000f87e", # humidity
      "\U0000e7b0", # co2
      "\U0000f55b", # poluttion
      ] 

lvgl:
  widgets:
    - label:
        text: "\U0000e846"
        text_font: font_icons

Paginado y navegación

Uno de los motivos por los que quería aprender a crear pantallas con LVGL en ESPHome es por la facilidad de navegación en sus interfaces.

La forma «ortodoxa» de hacerlo es encerrar nuestros widgets dentro de una página y asignarle un ‘id’. Luego tendríamos que añadir unos controles en el componente táctil para que al pulsar en el margen izquierdo o derecho de la pantalla cambie de página.

binary_sensor:

  - platform: touchscreen
    name: "Previous Page"
    internal: true
    x_min: 0
    x_max: 50
    y_min: 0
    y_max: 480
    on_press:
      then:
        lvgl.page.previous:
  - platform: touchscreen
    name: "Next Page"
    internal: true
    x_min: 430
    x_max: 480
    y_min: 0
    y_max: 480
    on_press:
      then:
        lvgl.page.next:

lvgl:
  pages:
    - id: pagina1
      widgets:
    - id: pagina2
      widgets:

Otra opción, si prefieres navegar deslizando el dedo por la pantalla, es simplemente dibujar elementos fuera de la misma (excediendo la resolución de la pantalla). Automáticamente LVGL creará un ‘scroll’ para acceder a esta parte «oculta».

Apagado automático de la pantalla

Una de las funciones más interesantes que he realizado creando dispositivos con pantalla, es apagarla cuando está inactiva. Es decir, que se apague automáticamente cuando no la tocas, y se encienda cuando si. La verdad es que hacer esto con LVGL es muy sencillo!

light:
  - platform: monochromatic
    output: ledc_gpio4
    name: Display Backlight
    id: display_backlight
    restore_mode: ALWAYS_ON

output:
  - platform: ledc
    pin: 
      number: GPIO45
      ignore_strapping_warning: true
    id: ledc_gpio4
    frequency: 100Hz

touchscreen:
  - platform: XXXXXX
    id: display_touchscreen
    on_release:
      then:
        - if:
            condition: lvgl.is_paused
            then:
              - light.turn_on: display_backlight
              - lvgl.resume:
              - lvgl.widget.redraw:

lvgl:
  touchscreens: ft5x06_touchscreen
  buffer_size: 100%
  bg_color: 0x062e20
  on_idle:
    - timeout: 10s
      then:
        - logger.log: idle timeout
        - if:
            condition:
              lvgl.is_idle:
                timeout: 5s
            then:
              - logger.log: LVGL is idle
    - timeout: 60s
      then:
        - logger.log: idle 60s timeout
        - lvgl.pause:
        - light.turn_off:
            id: display_backlight
            transition_length: 5s

Agradecimientos

Para elaborar esta entrada nos hemos apoyado en este video de nuestro amigo Luis de ProgramarFácil, canal que te recomiendo totalmente! 😎