Ir al contenido

Lopaka Basics 08: UI táctil con LVGL ​

Una pantalla táctil parecida a un teléfono mostrando un control de luz Living room con un interruptor de energía, un dial al 70% Brightness, un slider Warm a Cool y botones Relax y Focus

Esto parece una app en un teléfono, pero se ejecuta en un microcontrolador con una pantalla táctil pequeña. Giras el dial con un dedo, deslizas de cálido a frío, tocas un botón y el dial barre hasta un valor nuevo.

En este episodio verás cómo un framework de UI llamado LVGL convierte una pantalla pequeña en una interfaz táctil, y qué lo hace distinto de las bibliotecas de gráficos de los episodios anteriores. Después diseñarás la pantalla en Lopaka, añadirás unas líneas de código tuyas para los toques y lo ejecutarás en un ESP32 simulado con un panel táctil real.

Lo que vas a crear: una pantalla táctil de 240×320 que controla una luz del salón: un dial de brillo con un número grande, un slider Warm ↔ Cool, un interruptor de energía y dos botones de escena, Relax y Focus. Lo que necesitas: un navegador y el simulador en línea del episodio 1. Una placa real es opcional: un ESP32 y una pantalla ILI9341 de 2,8 pulgadas con un panel táctil capacitivo FT6206.

Un lápiz, o un conjunto de piezas ​

Una biblioteca de gráficos dibuja una imagen de un botón ​

Hasta ahora dibujamos cada pantalla con una biblioteca de gráficos como U8g2 o Adafruit GFX. Una biblioteca de gráficos es un lápiz: un rectángulo aquí, un texto allá. Un botón dibujado así es solo una imagen de un botón.

El toque también es tu problema. Cuando un dedo cae en la pantalla, tu código tiene que leer las coordenadas, comprobar si caen dentro del rectángulo (x > 124 && x < 224 && y > 274 …), dibujar el botón en su aspecto pulsado y redibujar la pantalla, todo a mano.

Un framework de UI te da objetos reales ​

LVGL es un framework de UI. No dibujas un botón, lo creas: lv_button_create(). Desde entonces el botón es un objeto que conoce su propio tamaño, colores y estado.

LVGL lee el toque, encuentra el objeto bajo el dedo, lo muestra pulsado y le dice a tu código qué ocurrió: «Focus fue clicado». También redibuja solo la parte de la pantalla que cambió, no toda la imagen.

Izquierda, una biblioteca de gráficos: el botón es solo una imagen, y el código comprueba las coordenadas y redibuja todo. Derecha, LVGL: lv_button_create crea un objeto real que se encuentra bajo el dedo, se muestra pulsado, le dice a tu código CLICKED → on_focus() y se redibuja solo él

Paso 1: un proyecto LVGL nuevo ​

Haz clic en + New Project y configura el diálogo:

  • Platform: LVGL (v9).
  • Title: «Living Room».
  • Screen Size: activa Custom y escribe W 240, H 320. La pantalla está vertical, como un teléfono.
  • Background Color: #0E1116, un negro azulado oscuro.

Haz clic en Save. El color de fondo es la primera línea del código generado:

cpp
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);

lv_screen_active() es el propio objeto de pantalla, y cada widget que añadas vivirá en él.

Paso 2: añade los widgets ​

En un proyecto LVGL la barra de herramientas guarda widgets reales: junto a la herramienta de selección y la pintura están text (Aa), Text Area, Panel, Button, Switch, Slider, Checkbox y Arc. Haz clic en una herramienta y el widget aparece en la pantalla. Después pon su posición, tamaño y colores en el inspector de la derecha.

El dial ​

Elige la herramienta Arc. Pon X 30, Y 52, W 180 y H 180, después Value 70. Pon Fill color en #2196F3 y Back en #2A3039.

El editor de Lopaka con la barra de herramientas de widgets LVGL; la capa de arco dial está seleccionada y el inspector muestra X 30, Y 52, W 180, H 180, Rotate 135, Start 0, End 270, Value 70, Fill color #2196F3 y Back #2A3039

Un dial de 180 píxeles en una pantalla de 240 píxeles deja 30 píxeles a cada lado, así que X 30 lo centra. Los otros tres números vienen con el arco:

  • Start 0 y End 270 hacen que la pista sea tres cuartos de círculo.
  • Rotate 135 lo gira para que el hueco quede abajo. LVGL cuenta los ángulos en sentido horario desde la derecha, así que el arco empieza en la parte inferior izquierda y termina en la parte inferior derecha.
  • Value 70 es cuánto de la pista está lleno, en una escala de 0 a 100.

Fill color pinta la parte llena y Back el resto de la pista.

Etiquetas, un interruptor, un slider y dos botones ​

Añade el resto de la pantalla. Dale a cada widget sus colores en el inspector sobre la marcha:

  • El número: un texto 70% en blanco (#FFFFFF) dentro del dial, en X 104, Y 150.
  • El título: un texto Living room en blanco en la esquina superior izquierda.
  • Un interruptor en la esquina superior derecha, 52×28 píxeles, con Fill color #2196F3 y Back #3A404A. Checked está activado: la luz empieza encendida.
  • Un slider bajo el dial, 208 píxeles de ancho y 8 de alto, con Fill color #2196F3.
  • Dos botones en la parte inferior, Relax y Focus, 100×36 píxeles cada uno. Pon R en 17, el color de texto en #FFFFFF y Back en #2A3039. Un radio de casi la mitad de la altura convierte cada botón en una pastilla.
  • Tres etiquetas pequeñas en gris (#8A93A0): Brightness bajo el número, Warm y Cool sobre los dos extremos del slider.

Cada widget aparece en el centro de la pantalla cuando lo añades, y los campos X e Y lo mueven a su sitio.

Paso 3: nombra las capas ​

Cada widget es una capa en el panel Capas, y el nombre de la capa se convierte en el nombre de variable en el código. Haz doble clic en una capa y dales a las seis que necesitarás en tu código nombres cortos:

LayerNameIn the code
the arcdialarc_dial
the 70% textlevellabel_level
the switchpowerswitch_power
the sliderwarmthslider_warmth
the Relax buttonrelaxbutton_relax
the Focus buttonfocusbutton_focus

Lopaka pone el tipo de widget delante del nombre. El título y las etiquetas grises mantienen sus nombres predeterminados; nada se refiere a ellos.

Todo es un objeto ​

LVGL ve la pantalla que creaste como un conjunto de objetos. Todo en la pantalla es un objeto, y los objetos viven en un árbol. La pantalla guarda el dial, el número, el slider, el interruptor y los botones. Cada botón guarda su propia etiqueta, así que los dos se mueven juntos.

La pantalla Living room junto a su árbol de objetos: la pantalla guarda arc_dial, label_level, slider_warmth, switch_power, button_relax y button_focus, cada botón guarda una etiqueta, más cuatro etiquetas más

Estilos y estados ​

Cada objeto tiene estilos: color, radio, fuente. Los estilos pueden depender del estado del objeto. El mismo botón Focus se ve distinto por defecto, mientras está pulsado y cuando está desactivado, y el interruptor cambia cuando está marcado. LVGL cambia entre estos aspectos por sí solo.

Estilos del botón Focus: color, radio y fuente, y sus tres estados default, pressed y disabled; debajo, el interruptor apagado y marcado

Eventos ​

Por último, eventos. Cuando el slider se mueve, envía un evento VALUE_CHANGED. Cuando se toca un botón, envía CLICKED. Tu función se ejecuta y decide qué pasa después: on_warmth() tinta el dial, on_focus() lo envía al 100%.

Lopaka escribe los objetos y sus estilos. Los eventos son tuyos.

Eventos: el slider envía VALUE_CHANGED a on_warmth(), que tinta el dial; el botón Focus envía CLICKED a on_focus(), que pone el dial al 100%. Lopaka escribe objetos y estilos, tú escribes los eventos

Paso 4: tu código ​

Lo que genera Lopaka ​

Abre el panel de código. Cada widget es una llamada de creación, seguida de su posición, tamaño y colores:

cpp
// [BEGIN lopaka generated]

lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);

// arcLvgl: dial
lv_obj_t * arc_dial = lv_arc_create(lv_screen_active());
lv_obj_set_pos(arc_dial, 30, 52);
lv_obj_set_size(arc_dial, 180, 180);
lv_arc_set_rotation(arc_dial, 135);
lv_arc_set_bg_angles(arc_dial, 0, 270);
lv_arc_set_value(arc_dial, 70);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2A3039), LV_PART_MAIN);

// string: level
lv_obj_t * label_level = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_level, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_level, 104, 132);
lv_label_set_text(label_level, "70%");

// string: string 3
lv_obj_t * label_string_3 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_3, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_string_3, 16, 14);
lv_label_set_text(label_string_3, "Living room");

// switch: power
lv_obj_t * switch_power = lv_switch_create(lv_screen_active());
lv_obj_set_pos(switch_power, 172, 12);
lv_obj_set_size(switch_power, 52, 28);
lv_obj_add_state(switch_power, LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x2196F3), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x3A404A), LV_PART_MAIN);

// slider: warmth
lv_obj_t * slider_warmth = lv_slider_create(lv_screen_active());
lv_slider_set_value(slider_warmth, 50, LV_ANIM_OFF);
lv_obj_set_pos(slider_warmth, 16, 252);
lv_obj_set_size(slider_warmth, 208, 8);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_KNOB);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_MAIN);

// button: relax
lv_obj_t * button_relax = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_relax, 16, 274);
lv_obj_set_size(button_relax, 100, 36);
lv_obj_set_style_radius(button_relax, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_relax, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_relax_label = lv_label_create(button_relax);
lv_label_set_text(button_relax_label, "Relax");
lv_obj_center(button_relax_label);

// button: focus
lv_obj_t * button_focus = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_focus, 124, 274);
lv_obj_set_size(button_focus, 100, 36);
lv_obj_set_style_radius(button_focus, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_focus, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_focus_label = lv_label_create(button_focus);
lv_label_set_text(button_focus_label, "Focus");
lv_obj_center(button_focus_label);

// string: string 8
lv_obj_t * label_string_8 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_8, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_8, 84, 188);
lv_label_set_text(label_string_8, "Brightness");

// string: string 9
lv_obj_t * label_string_9 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_9, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_9, 16, 226);
lv_label_set_text(label_string_9, "Warm");

// string: string 10
lv_obj_t * label_string_10 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_10, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_10, 194, 226);
lv_label_set_text(label_string_10, "Cool");
// [END lopaka generated]

Este es el código con Wrapper function desactivado, como lo necesitaremos en un momento. Algunos detalles en él:

  • Partes. Un arco tiene una pista (LV_PART_MAIN) y una parte llena (LV_PART_INDICATOR); un slider y un interruptor también tienen un mando (LV_PART_KNOB). Cada llamada de estilo nombra la parte que pinta. El color del interruptor se aplica a LV_PART_INDICATOR | LV_STATE_CHECKED, la parte llena mientras el interruptor está activado: un estilo para un estado.
  • El botón y su etiqueta. lv_label_create(button_relax) crea la etiqueta dentro del botón, no en la pantalla, y lv_obj_center() la mantiene en el centro. Ese es el árbol de objetos de la explicación, en código.
  • Por qué el número dice 132. lv_obj_set_pos() coloca un objeto por su esquina superior izquierda. En el inspector de Lopaka, la Y de un texto es la parte inferior de su línea, como en cada plataforma. La línea de texto tiene 18 píxeles de alto, así que Y 150 en el inspector se convierte en 132 en el código.
  • Los valores predeterminados quedan fuera. Lopaka omite los estilos que coinciden con los valores predeterminados de LVGL. Por eso los botones no tienen una línea para su texto blanco: el blanco ya es el color predeterminado del texto de un botón.

Desactiva la función envolvente ​

Con Wrapper function activado, Lopaka envuelve el código en void drawScreen_1(void) { … }. Desactívalo: entonces las líneas generadas se sitúan dentro de una función que escribes tú, con tu propio código arriba y abajo de ellas. Lopaka mantiene lo que escribas fuera de los marcadores BEGIN y END, y actualiza solo la parte entre ellos cuando cambias el diseño.

Tu código sobre los marcadores ​

Sobre // [BEGIN lopaka generated], escribe la configuración de pantalla y unas funciones pequeñas:

cpp
#include "touch_display.h"

lv_obj_t *dial, *level, *warmth;

void show_level(void *obj, int32_t v) {
  lv_arc_set_value(dial, v);
  lv_label_set_text_fmt(level, "%d%%", v);
}

void tint(int w) {
  lv_color_t warm = lv_color_hex(0xFF8A3D), white = lv_color_hex(0xFFF1DC), cool = lv_color_hex(0x4FC3F7);
  lv_color_t c = w < 50 ? lv_color_mix(white, warm, w * 255 / 50)
                        : lv_color_mix(cool, white, (w - 50) * 255 / 50);
  lv_obj_set_style_arc_color(dial, c, LV_PART_INDICATOR);
  lv_obj_set_style_bg_color(dial, c, LV_PART_KNOB);
}

void set_warmth(void *obj, int32_t w) {
  lv_slider_set_value(warmth, w, LV_ANIM_OFF);
  tint(w);
}

void scene(int to_level, int to_warmth) {
  lv_anim_t a;
  lv_anim_init(&a);
  lv_anim_set_duration(&a, 700);
  lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
  lv_anim_set_exec_cb(&a, show_level);
  lv_anim_set_values(&a, lv_arc_get_value(dial), to_level);
  lv_anim_start(&a);
  lv_anim_set_exec_cb(&a, set_warmth);
  lv_anim_set_values(&a, lv_slider_get_value(warmth), to_warmth);
  lv_anim_start(&a);
}

void on_dial(lv_event_t *e) { show_level(NULL, lv_arc_get_value(dial)); }
void on_warmth(lv_event_t *e) { tint(lv_slider_get_value(warmth)); }
void on_relax(lv_event_t *e) { scene(30, 10); }
void on_focus(lv_event_t *e) { scene(100, 90); }

void on_power(lv_event_t *e) {
  lv_obj_t *sw = lv_event_get_target_obj(e);
  bool on = lv_obj_has_state(sw, LV_STATE_CHECKED);
  lv_obj_t *scr = lv_screen_active();
  for (uint32_t i = 0; i < lv_obj_get_child_count(scr); i++) {
    lv_obj_t *o = lv_obj_get_child(scr, i);
    if (o == sw) continue;
    lv_obj_set_state(o, LV_STATE_DISABLED, !on);
    lv_obj_set_style_opa(o, on ? LV_OPA_COVER : LV_OPA_30, 0);
  }
}

void build_ui() {
  • show_level() mueve el dial y escribe el número: %d%% imprime el valor seguido de un símbolo de porcentaje.
  • tint() convierte la posición del slider, de 0 a 100, en un color: naranja cálido en 0, blanco cálido en 50, azul frío en 100. lv_color_mix() mezcla dos colores; su último argumento, de 0 a 255, dice cuánto del primer color tomar.
  • scene() anima el dial y el slider a valores nuevos en 700 milisegundos, ralentizando al final. Relax va al 30% y cálido, Focus al 100% y frío.
  • on_power() se ejecuta cuando el interruptor cambia. Con la luz apagada, desactiva cada otro objeto en la pantalla y lo desvanece al 30%.

La última línea abre build_ui(), la función en la que ahora se sitúa el código generado.

Tu código bajo los marcadores ​

Bajo // [END lopaka generated], cierra la función y añade los estilos, los eventos y el habitual setup() y loop():

cpp
  dial = arc_dial;
  level = label_level;
  warmth = slider_warmth;

  // styles
  lv_obj_set_style_text_font(level, &lv_font_montserrat_48, 0);
  lv_obj_align(level, LV_ALIGN_TOP_MID, 0, 116);
  lv_obj_set_style_bg_color(warmth, lv_color_hex(0xFF8A3D), LV_PART_MAIN);
  lv_obj_set_style_bg_opa(warmth, LV_OPA_COVER, LV_PART_MAIN);
  lv_obj_set_style_bg_grad_color(warmth, lv_color_hex(0x4FC3F7), LV_PART_MAIN);
  lv_obj_set_style_bg_grad_dir(warmth, LV_GRAD_DIR_HOR, LV_PART_MAIN);
  lv_obj_set_style_bg_opa(warmth, LV_OPA_TRANSP, LV_PART_INDICATOR);
  lv_obj_set_style_bg_color(warmth, lv_color_white(), LV_PART_KNOB);
  tint(lv_slider_get_value(warmth));

  // events
  lv_obj_add_event_cb(arc_dial, on_dial, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(slider_warmth, on_warmth, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(switch_power, on_power, LV_EVENT_VALUE_CHANGED, NULL);
  lv_obj_add_event_cb(button_relax, on_relax, LV_EVENT_CLICKED, NULL);
  lv_obj_add_event_cb(button_focus, on_focus, LV_EVENT_CLICKED, NULL);
}

void setup() {
  display_begin();
  build_ui();
}

void loop() {
  lv_timer_handler();
  delay(5);
}

El panel de código en Lopaka con Wrapper function desactivado: bajo el marcador END, el bloque de estilos con la fuente Montserrat 48 y el degradado del slider, después cinco líneas lv_obj_add_event_cb, setup y loop

Las tres primeras líneas copian los objetos generados en las variables globales, para que las funciones de arriba puedan alcanzarlos. Las variables generadas existen solo dentro de build_ui().

Estilos. Una línea de estilo hace el número grande: lv_font_montserrat_48 es la fuente Montserrat de LVGL a 48 píxeles. Un número más grande también es más ancho, así que lv_obj_align() lo centra en la parte superior del dial en lugar de dejarlo en su antigua esquina superior izquierda. Las líneas siguientes pintan la pista del slider como un degradado de cálido a frío, ocultan su parte llena y hacen el mando blanco.

Eventos. Una línea por widget lo conecta a una función: lv_obj_add_event_cb(object, function, event, NULL). El dial actualiza el número, el slider tinta el dial, los botones cambian de escena y el interruptor atenúa la pantalla. El dial, el slider y el interruptor envían LV_EVENT_VALUE_CHANGED; los botones envían LV_EVENT_CLICKED.

El loop. lv_timer_handler() es donde LVGL hace su trabajo: lee el toque, envía los eventos, ejecuta las animaciones y redibuja lo que cambió. Llámalo cada pocos milisegundos y mantén loop() libre de retardos largos, o la pantalla deja de reaccionar.

Paso 5: copia el código ​

Haz clic en Copy sobre el código. Obtienes todo el sketch: tu código, la parte generada entre los marcadores, y setup() y loop().

Paso 6: ejecútalo ​

El archivo de pantalla ​

El sketch empieza con #include "touch_display.h". Ese archivo conecta LVGL con esta pantalla y su chip táctil. Es el mismo para cada proyecto con esta pantalla, así que lo escribes una vez:

cpp
// Display + touch glue for LVGL: ILI9341 (SPI) + FT6206 (I2C), 240x320 portrait.
#include <lvgl.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_FT6206.h>

Adafruit_ILI9341 tft(23, 21, 22);  // CS, DC, RST
Adafruit_FT6206 touch;
#define DRAW_BUF_SIZE (240 * 160 * 2)   // half the screen
static uint8_t *draw_buf;

static void flush(lv_display_t *d, const lv_area_t *a, uint8_t *px) {
  uint32_t w = a->x2 - a->x1 + 1, h = a->y2 - a->y1 + 1;
  tft.startWrite();
  tft.setAddrWindow(a->x1, a->y1, w, h);
  tft.writePixels((uint16_t *)px, w * h);
  tft.endWrite();
  lv_display_flush_ready(d);
}

static void read_touch(lv_indev_t *i, lv_indev_data_t *data) {
  if (touch.touched()) {
    TS_Point p = touch.getPoint();
    data->point.x = 240 - p.x;   // the touch panel is mirrored
    data->point.y = 320 - p.y;
    data->state = LV_INDEV_STATE_PRESSED;
  } else {
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

void display_begin() {
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH);  // backlight
  SPI.begin(18, 17, 19);  // SCK, MISO, MOSI
  Wire.begin(4, 16);      // SDA, SCL
  tft.begin();
  tft.setRotation(0);
  touch.begin(40);
  lv_init();
  lv_tick_set_cb(millis);
  lv_display_t *disp = lv_display_create(240, 320);
  lv_display_set_flush_cb(disp, flush);
  draw_buf = (uint8_t *)malloc(DRAW_BUF_SIZE);
  lv_display_set_buffers(disp, draw_buf, NULL, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
  lv_indev_t *indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  lv_indev_set_read_cb(indev, read_touch);
}

Tiene dos trabajos:

  • flush() envía píxeles a la pantalla. LVGL no guarda toda la pantalla en memoria: una imagen completa de 240×320 con dos bytes por píxel ocuparía 153.600 bytes. Dibuja en un búfer de la mitad de la pantalla, 240 × 160 × 2 = 76.800 bytes, y entrega una zona cada vez. Cuando solo cambia el número, solo esa zona se dibuja y se envía.
  • read_touch() le dice a LVGL dónde está el dedo. El chip FT6206 informa el punto de toque. En este panel sus coordenadas van en sentido opuesto a las de la pantalla, así que 240 - p.x y 320 - p.y les dan la vuelta.

lv_tick_set_cb(millis) le da a LVGL un reloj para sus animaciones, y touch.begin(40) pone qué firme tiene que ser un toque para contar.

El archivo de ajustes ​

LVGL lee sus ajustes de un archivo llamado lv_conf.h. Toma lv_conf_template.h de la biblioteca LVGL, guárdalo como lv_conf.h y cambia dos líneas:

cpp
#if 1 /* Set this to "1" to enable content */
cpp
#define LV_FONT_MONTSERRAT_48 1

La primera línea activa el archivo. La segunda añade la fuente de 48 píxeles para el número grande: LVGL compila solo las fuentes que activas, y la plantilla activa solo Montserrat 14, la fuente predeterminada de cada etiqueta.

Ejecútalo en el simulador ​

En el simulador en línea del episodio 1, crea un proyecto ESP32 nuevo y añade la pantalla ILI9341 Cap Touch: una pantalla ILI9341 con un panel táctil FT6206. Cablea la pantalla al ESP32:

  • VCC a 3V3, GND a GND, LED (la retroiluminación) a 5,
  • CS a 23, D/C a 21, RST a 22,
  • SCK a 18, MOSI a 19, MISO a 17,
  • SDA a 4 y SCL a 16 para el panel táctil.

Añade las bibliotecas lvgl, Adafruit ILI9341 y Adafruit FT6206 Library. Crea dos archivos nuevos, touch_display.h y lv_conf.h, con los contenidos de arriba. Pega el código de Lopaka en sketch.ino y pulsa Play.

Cuando se compila, el ratón se convierte en un dedo. Gira el dial: el número sigue. Desliza de cálido a frío: el dial cambia de color. Toca Focus: el dial barre al 100%. Después apaga la luz y el resto de la pantalla se desvanece.

El ESP32 simulado cableado a una pantalla ILI9341 Cap Touch mostrando la pantalla Living room con el dial al 43%, un cursor en el mando del dial

En una placa real ​

El mismo sketch se ejecuta en un ESP32 real con un módulo ILI9341 de 2,8 pulgadas. En el IDE de Arduino, instala las mismas tres bibliotecas y elige la placa ESP32 Dev Module. El sketch ocupa aproximadamente la mitad de la flash y el 27% de la RAM; 64 KB de esa RAM es la memoria propia de LVGL para objetos y estilos. Tres cosas que comprobar:

  • Dónde va lv_conf.h. En el IDE de Arduino no va junto al sketch. Ponlo en tu carpeta libraries, junto a la carpeta lvgl, no dentro de ella. Si LVGL no puede encontrarlo, falta la fuente de 48 píxeles y el sketch no compila.
  • El chip táctil. Muchos módulos ILI9341 económicos de 2,8 pulgadas tienen un panel táctil resistivo con un chip XPT2046 en lugar del capacitivo FT6206. Para esos, read_touch() necesita una biblioteca para el XPT2046; el resto del sketch se mantiene igual.
  • La dirección del toque. Si un toque cae en el lado equivocado de la pantalla, tu panel no está espejado como este. Usa p.x y p.y directamente en read_touch(), o espeja solo uno de ellos.

Cuándo vale la pena LVGL ​

LVGL necesita memoria. Su propia documentación pide al menos 32 KB de RAM. Un Arduino UNO tiene 2 KB, así que LVGL solo necesita 16 veces todo el UNO.

Si LVGL siempre vale la pena: la barra naranja LVGL necesita al menos 32 KB es 16 veces más larga que los 2 KB de RAM de un Arduino UNO

Un ESP32 le da a tu sketch 320 KB de RAM y esta interfaz usa el 27% de ella. Elige la herramienta según el trabajo:

  • ¿Unos números en un OLED? Una biblioteca de gráficos basta.
  • ¿Toque, widgets y animación? Eso es LVGL.

En un ESP32 con 320 KB para tu sketch, a LVGL le sobra sitio. Unos números en un OLED: biblioteca de gráficos. Toque, widgets, animación: LVGL

Resumen ​

  1. Objetos, no píxeles: LVGL crea una pantalla a partir de objetos como arcos, sliders y botones. Creas un botón en lugar de dibujarlo.
  2. Estilos para cada estado: colores, radio y fuentes deciden cómo se ve un objeto por defecto, mientras está pulsado, marcado o desactivado.
  3. Los eventos conectan un toque con tu código: una línea lv_obj_add_event_cb por widget. Lopaka diseña los widgets y tú escribes los eventos.

Próximo episodio: Beyond Arduino. Un diseño, muchas plataformas, de ESPHome a MicroPython y Flipper Zero. Lee el episodio 9 o míralo en YouTube.

¿Listo para probar? Abre Lopaka y diseña tu propia pantalla táctil.