Pular para o conteúdo

Lopaka Basics 08: UI sensível ao toque com LVGL ​

Uma tela sensível ao toque parecida com um celular mostrando um controle de luz Living room com um interruptor de energia, um dial em 70% Brightness, um controle deslizante Warm to Cool e botões Relax e Focus

Isso parece um aplicativo em um celular, mas roda em um microcontrolador com uma telinha sensível ao toque. Você gira o dial com um dedo, desliza de quente para frio, toca em um botão e o dial varre para um novo valor.

Neste episódio, você vai ver como um framework de UI chamado LVGL transforma uma tela pequena em uma interface sensível ao toque, e o que torna ele diferente das bibliotecas gráficas dos episódios anteriores. Depois você vai organizar a tela no Lopaka, adicionar algumas linhas do seu próprio código para os toques e executar ele em um ESP32 simulado com um painel de toque real.

O que você vai construir: uma tela sensível ao toque de 240×320 que controla uma luz da sala de estar: um dial de brilho com um número grande, um controle deslizante Warm ↔ Cool, um interruptor de energia e dois botões de cena, Relax e Focus. Do que você precisa: um navegador e o simulador online do episódio 1. Uma placa real é opcional: um ESP32 e um display ILI9341 de 2,8 polegadas com um painel de toque capacitivo FT6206.

Uma caneta ou um conjunto de peças ​

Uma biblioteca gráfica desenha uma imagem de um botão ​

Até agora, desenhamos cada tela com uma biblioteca gráfica como U8g2 ou Adafruit GFX. Uma biblioteca gráfica é uma caneta: um retângulo aqui, um texto ali. Um botão desenhado assim é só uma imagem de um botão.

O toque também é problema seu. Quando um dedo pousa na tela, seu código precisa ler as coordenadas, conferir se elas caem dentro do retângulo (x > 124 && x < 224 && y > 274 …), desenhar o botão no visual pressionado dele e redesenhar a tela, tudo à mão.

Um framework de UI oferece objetos reais ​

LVGL é um framework de UI. Você não desenha um botão, você cria um: lv_button_create(). A partir daí, o botão é um objeto que conhece o próprio tamanho, as próprias cores e o próprio estado.

O LVGL lê o toque, encontra o objeto sob o dedo, mostra ele pressionado e diz ao seu código o que aconteceu: "Focus was clicked". Ele também redesenha só a parte da tela que mudou, e não a imagem inteira.

À esquerda, uma biblioteca gráfica: o botão é só uma imagem, e o código confere as coordenadas e redesenha tudo. À direita, LVGL: lv_button_create faz um objeto real que se encontra sob o dedo, mostra ele pressionado, diz ao seu código CLICKED → on_focus() e redesenha só ele mesmo

Etapa 1: um novo projeto LVGL ​

Clique em + New Project e configure a caixa de diálogo:

  • Platform: LVGL (v9).
  • Title: "Living Room".
  • Screen Size: ative Custom e digite W 240, H 320. A tela fica em pé, como um celular.
  • Background Color: #0E1116, um preto azulado escuro.

Clique em Salvar. A cor de fundo é a primeira linha do código gerado:

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

lv_screen_active() é o próprio objeto da tela, e cada widget que você adicionar vai morar nele.

Etapa 2: adicione os widgets ​

Em um projeto LVGL, a barra de ferramentas guarda widgets reais: ao lado da ferramenta de seleção e da pintura ficam text (Aa), Text Area, Panel, Button, Switch, Slider, Checkbox e Arc. Clique em uma ferramenta e o widget aparece na tela. Depois defina a posição, o tamanho e as cores dele no inspetor à direita.

O dial ​

Escolha a ferramenta Arc. Defina X 30, Y 52, W 180 e H 180, depois Value 70. Defina Fill color como #2196F3 e Back como #2A3039.

O editor do Lopaka com a barra de ferramentas de widgets LVGL; a camada arc dial está selecionada e o inspetor mostra X 30, Y 52, W 180, H 180, Rotate 135, Start 0, End 270, Value 70, Fill color #2196F3 e Back #2A3039

Um dial de 180 pixels em uma tela de 240 pixels deixa 30 pixels de cada lado, então X 30 centraliza ele. Os outros três números vêm com o arco:

  • Start 0 e End 270 tornam a trilha três quartos de um círculo.
  • Rotate 135 gira ele para a abertura ficar na parte inferior. O LVGL conta os ângulos no sentido horário a partir da direita, então o arco começa no canto inferior esquerdo e termina no canto inferior direito.
  • Value 70 é quanto da trilha está preenchido, em uma escala de 0 a 100.

Fill color pinta a parte preenchida e Back, o resto da trilha.

Rótulos, um interruptor, um controle deslizante e dois botões ​

Adicione o resto da tela. Dê a cada widget as cores dele no inspetor enquanto avança:

  • O número: um texto 70% em branco (#FFFFFF) dentro do dial, em X 104, Y 150.
  • O título: um texto Living room em branco no canto superior esquerdo.
  • Um interruptor no canto superior direito, 52×28 pixels, com Fill color #2196F3 e Back #3A404A. Checked está ativado: a luz começa ligada.
  • Um controle deslizante sob o dial, 208 pixels de largura e 8 de altura, com Fill color #2196F3.
  • Dois botões na parte inferior, Relax e Focus, 100×36 pixels cada. Defina R como 17, a cor do texto como #FFFFFF e Back como #2A3039. Um raio de quase metade da altura transforma cada botão em uma pílula.
  • Três rótulos pequenos em cinza (#8A93A0): Brightness sob o número, Warm e Cool acima das duas extremidades do controle deslizante.

Cada widget aparece no meio da tela quando você adiciona ele, e os campos X e Y movem ele para o lugar.

Etapa 3: nomeie as camadas ​

Cada widget é uma camada no painel Camadas, e o nome da camada vira o nome da variável no código. Clique duas vezes em uma camada e dê aos seis de que você vai precisar no seu código nomes curtos:

CamadaNomeNo código
the arcdialarc_dial
the 70% textlevellabel_level
the switchpowerswitch_power
the sliderwarmthslider_warmth
the Relax buttonrelaxbutton_relax
the Focus buttonfocusbutton_focus

O Lopaka coloca o tipo do widget na frente do nome. O título e os rótulos cinza mantêm os nomes padrão deles; nada faz referência a eles.

Tudo é um objeto ​

O LVGL vê a tela que você montou como um conjunto de objetos. Tudo na tela é um objeto, e os objetos moram em uma árvore. A tela guarda o dial, o número, o controle deslizante, o interruptor e os botões. Cada botão guarda o próprio rótulo, então os dois se movem juntos.

A tela Living room ao lado da árvore de objetos dela: screen guarda arc_dial, label_level, slider_warmth, switch_power, button_relax e button_focus, cada botão guarda um rótulo, além de mais quatro rótulos

Estilos e estados ​

Cada objeto tem estilos: cor, raio, fonte. Os estilos podem depender do estado do objeto. O mesmo botão Focus parece diferente por padrão, enquanto está pressionado e quando está desativado, e o interruptor muda quando está marcado. O LVGL alterna entre esses visuais sozinho.

Estilos do botão Focus: cor, raio e fonte, e os três estados dele default, pressed e disabled; abaixo, o interruptor desligado e marcado

Eventos ​

Por fim, eventos. Quando o controle deslizante se move, ele envia um evento VALUE_CHANGED. Quando um botão é tocado, ele envia CLICKED. Sua função executa e decide o que acontece depois: on_warmth() tinge o dial, on_focus() envia ele para 100%.

O Lopaka escreve os objetos e os estilos deles. Os eventos são seus.

Eventos: o controle deslizante envia VALUE_CHANGED para on_warmth(), que tinge o dial; o botão Focus envia CLICKED para on_focus(), que define o dial como 100%. O Lopaka escreve objetos e estilos, você escreve os eventos

Etapa 4: seu código ​

O que o Lopaka gera ​

Abra o painel de código. Cada widget é uma chamada de criação, seguida da posição, do tamanho e das cores dele:

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 é o código com Wrapper function desativado, como vamos precisar dele daqui a pouco. Alguns detalhes nele:

  • Partes. Um arco tem uma trilha (LV_PART_MAIN) e uma parte preenchida (LV_PART_INDICATOR); um controle deslizante e um interruptor também têm um botão (LV_PART_KNOB). Cada chamada de estilo nomeia a parte que ela pinta. A cor do interruptor se aplica a LV_PART_INDICATOR | LV_STATE_CHECKED, a parte preenchida enquanto o interruptor está ligado: um estilo para um estado.
  • O botão e o rótulo dele. lv_label_create(button_relax) cria o rótulo dentro do botão, e não na tela, e lv_obj_center() mantém ele no meio. Essa é a árvore de objetos da explicação, em código.
  • Por que o número diz 132. lv_obj_set_pos() posiciona um objeto pelo canto superior esquerdo dele. No inspetor do Lopaka, o Y de um texto é a parte inferior da linha dele, como em cada plataforma. A linha de texto tem 18 pixels de altura, então Y 150 no inspetor vira 132 no código.
  • Padrões ficam de fora. O Lopaka deixa de fora estilos que correspondem aos próprios padrões do LVGL. É por isso que os botões não têm uma linha para o texto branco deles: branco já é a cor padrão do texto de um botão.

Desative a função contenedora ​

Com Wrapper function ativado, o Lopaka envolve o código em void drawScreen_1(void) { … }. Desative ele: então as linhas geradas ficam dentro de uma função que você mesmo escreve, com seu próprio código acima e abaixo delas. O Lopaka mantém o que você digita fora dos marcadores BEGIN e END e atualiza só a parte entre eles quando você muda o design.

Seu código acima dos marcadores ​

Acima de // [BEGIN lopaka generated], digite a configuração do display e algumas funções pequenas:

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() move o dial e escreve o número: %d%% imprime o valor seguido de um sinal de porcentagem.
  • tint() transforma a posição do controle deslizante, de 0 a 100, em uma cor: laranja quente em 0, branco quente em 50, azul frio em 100. lv_color_mix() mistura duas cores; o último argumento dele, de 0 a 255, diz quanto da primeira cor pegar.
  • scene() anima o dial e o controle deslizante para novos valores em 700 milissegundos, desacelerando no fim. Relax vai para 30% e quente, Focus para 100% e frio.
  • on_power() executa quando o interruptor muda. Com a luz apagada, ele desativa cada outro objeto na tela e esmaece ele para 30%.

A última linha abre build_ui(), a função em que o código gerado agora fica.

Seu código abaixo dos marcadores ​

Abaixo de // [END lopaka generated], feche a função e adicione os estilos, os eventos e os usuais setup() e 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);
}

O painel de código no Lopaka com Wrapper function desativado: abaixo do marcador END, o bloco de estilos com a fonte Montserrat 48 e o gradiente do controle deslizante, depois cinco linhas lv_obj_add_event_cb, setup e loop

As três primeiras linhas copiam os objetos gerados para as variáveis globais, para as funções acima conseguirem alcançar eles. As variáveis geradas existem só dentro de build_ui().

Estilos. Uma linha de estilo torna o número grande: lv_font_montserrat_48 é a fonte Montserrat do LVGL em 48 pixels. Um número maior também é mais largo, então lv_obj_align() centraliza ele no topo do dial em vez de deixar ele no canto superior esquerdo antigo. As linhas seguintes pintam a trilha do controle deslizante como um gradiente de quente para frio, escondem a parte preenchida dele e tornam o botão branco.

Eventos. Uma linha por widget conecta ele a uma função: lv_obj_add_event_cb(object, function, event, NULL). O dial atualiza o número, o controle deslizante tinge o dial, os botões trocam de cena e o interruptor esmaece a tela. O dial, o controle deslizante e o interruptor enviam LV_EVENT_VALUE_CHANGED; os botões enviam LV_EVENT_CLICKED.

O loop. lv_timer_handler() é onde o LVGL faz o trabalho dele: ele lê o toque, envia os eventos, executa as animações e redesenha o que mudou. Chame ele a cada poucos milissegundos e mantenha loop() livre de delays longos, ou a tela para de reagir.

Etapa 5: copie o código ​

Clique em Copiar acima do código. Você recebe o sketch inteiro: seu código, a parte gerada entre os marcadores, e setup() e loop().

Etapa 6: execute ele ​

O arquivo do display ​

O sketch começa com #include "touch_display.h". Esse arquivo conecta o LVGL a este display e ao chip de toque dele. Ele é o mesmo para cada projeto com este display, então você escreve ele uma 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);
}

Ele tem dois trabalhos:

  • flush() envia pixels para o display. O LVGL não mantém a tela inteira na memória: uma imagem completa de 240×320 com dois bytes por pixel ocuparia 153.600 bytes. Ele desenha em um buffer de metade da tela, 240 × 160 × 2 = 76.800 bytes, e entrega uma área de cada vez. Quando só o número muda, só aquela área é desenhada e enviada.
  • read_touch() diz ao LVGL onde está o dedo. O chip FT6206 informa o ponto de toque. Neste painel, as coordenadas dele vão na direção oposta às do display, então 240 - p.x e 320 - p.y invertem elas.

lv_tick_set_cb(millis) dá ao LVGL um relógio para as animações dele, e touch.begin(40) define quão firme um toque precisa ser para contar.

O arquivo de configurações ​

O LVGL lê as configurações dele de um arquivo chamado lv_conf.h. Pegue lv_conf_template.h da biblioteca LVGL, salve ele como lv_conf.h e mude duas linhas:

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

A primeira linha ativa o arquivo. A segunda adiciona a fonte de 48 pixels para o número grande: o LVGL compila só as fontes que você ativa, e o modelo ativa só Montserrat 14, a fonte padrão de cada rótulo.

Execute ele no simulador ​

No simulador online do episódio 1, crie um novo projeto ESP32 e adicione o display ILI9341 Cap Touch: uma tela ILI9341 com um painel de toque FT6206. Ligue ele ao ESP32:

  • VCC ao 3V3, GND ao GND, LED (o backlight) ao 5,
  • CS ao 23, D/C ao 21, RST ao 22,
  • SCK ao 18, MOSI ao 19, MISO ao 17,
  • SDA ao 4 e SCL ao 16 para o painel de toque.

Adicione as bibliotecas lvgl, Adafruit ILI9341 e Adafruit FT6206 Library. Crie dois arquivos novos, touch_display.h e lv_conf.h, com o conteúdo acima. Cole o código do Lopaka em sketch.ino e pressione Play.

Quando ele estiver montado, o mouse vira um dedo. Gire o dial: o número acompanha. Deslize de quente para frio: o dial muda de cor. Toque em Focus: o dial varre para 100%. Depois desligue a luz, e o resto da tela esmaece.

O ESP32 simulado ligado a um display ILI9341 Cap Touch mostrando a tela Living room com o dial em 43%, um cursor no botão do dial

Em uma placa real ​

O mesmo sketch roda em um ESP32 real com um módulo ILI9341 de 2,8 polegadas. Na Arduino IDE, instale as mesmas três bibliotecas e escolha a placa ESP32 Dev Module. O sketch ocupa cerca de metade da flash e 27% da RAM; 64 KB dessa RAM é a própria memória do LVGL para objetos e estilos. Três coisas para conferir:

  • Onde lv_conf.h vai. Na Arduino IDE, ele não vai ao lado do sketch. Coloque ele na sua pasta libraries, ao lado da pasta lvgl, e não dentro dela. Se o LVGL não conseguir encontrar ele, a fonte de 48 pixels falta e o sketch não compila.
  • O chip de toque. Muitos módulos ILI9341 de 2,8 polegadas de baixo custo têm um painel de toque resistivo com um chip XPT2046 em vez do FT6206 capacitivo. Para esses, read_touch() precisa de uma biblioteca para o XPT2046; o resto do sketch continua o mesmo.
  • A direção do toque. Se um toque pousar no lado errado da tela, seu painel não é espelhado como este. Use p.x e p.y direto em read_touch(), ou espelhe só um deles.

Quando o LVGL vale a pena ​

O LVGL precisa de memória. A própria documentação dele pede pelo menos 32 KB de RAM. Um Arduino UNO tem 2 KB, então o LVGL sozinho precisa de 16 vezes o UNO inteiro.

O LVGL sempre vale a pena: a barra laranja de que o LVGL precisa de pelo menos 32 KB é 16 vezes mais longa que os 2 KB de RAM de um Arduino UNO

Um ESP32 dá ao seu sketch 320 KB de RAM, e esta interface usa 27% dela. Escolha a ferramenta pelo trabalho:

  • Alguns números em um OLED? Uma biblioteca gráfica basta.
  • Toque, widgets e animação? Isso é LVGL.

Em um ESP32 com 320 KB para o seu sketch, o LVGL deixa espaço de sobra. Alguns números em um OLED: biblioteca gráfica. Toque, widgets, animação: LVGL

Recapitulação ​

  1. Objetos, não pixels: o LVGL monta uma tela a partir de objetos como arcos, controles deslizantes e botões. Você cria um botão em vez de desenhar ele.
  2. Estilos para cada estado: cores, raio e fontes decidem como um objeto parece por padrão, enquanto está pressionado, marcado ou desativado.
  3. Eventos conectam um toque ao seu código: uma linha lv_obj_add_event_cb por widget. O Lopaka organiza os widgets, e você escreve os eventos.

Próximo episódio: Beyond Arduino. Um design, muitas plataformas, do ESPHome ao MicroPython e Flipper Zero. Leia o episódio 9 ou assista no YouTube.

Pronto para testar? Abra o Lopaka e organize sua própria tela sensível ao toque.