Pular para o conteúdo

Lopaka Basics 06: telas e menus ​

Um módulo OLED SSD1306 simulado mostrando um menu com as linhas Clock, Weather e Heart, a linha Weather destacada, acima de dois botões identificados como NEXT e OK

Clique: a próxima tela. Clique: e a seguinte. O outro botão abre um menu, e o cursor espera na página de onde você veio. Parece um gadget de verdade, e são quatro desenhos do Lopaka, dois botões e um sketch curto.

Neste episódio, você vai descobrir como cada menu funciona por dentro: um mapa de telas e botões que cabe em duas variáveis. Depois você vai desenhar as quatro telas no Lopaka, escrever o sketch que lê os botões e executar ele em um Arduino UNO simulado.

O que você vai construir: um gadget pequeno de relógio, clima e frequência cardíaca em um OLED de 128×64. O botão NEXT alterna entre três páginas, e OK abre um menu com um cursor. Do que você precisa: um navegador e a configuração do simulador do episódio 1: um Arduino UNO, um OLED SSD1306 e a biblioteca U8g2. Dois botões de pressão são novos. Uma placa real é opcional; a fiação é a mesma do episódio 5.

Um menu é um mapa ​

O dispositivo está sempre em uma tela ​

Cada menu em cada gadget funciona como um mapa. A qualquer momento, o dispositivo mostra exatamente uma tela. Qual tela é essa é o estado do dispositivo.

Cada botão é uma seta ​

Cada pressionamento de botão move o dispositivo de um estado para o próximo, como uma seta no mapa. Nosso gadget tem três páginas e um menu:

  • NEXT percorre as páginas: o relógio, o clima, a frequência cardíaca e de volta ao relógio.
  • OK salta para o menu, com o cursor na página de onde você veio.
  • No menu, NEXT move o cursor, e OK abre aquela página.

O mapa de estados: Clock, Weather e Heart em uma linha unidos por setas NEXT, com NEXT de Heart de volta para Clock; setas OK descem de Weather para o menu e voltam; o menu tem seu próprio NEXT; você está aqui marca Weather

Desenhe o mapa primeiro, e o código quase se escreve sozinho.

Duas variáveis guardam o mapa inteiro ​

O mapa inteiro cabe em duas variáveis:

  • page: qual página, 0 para o relógio, 1 para o clima, 2 para a frequência cardíaca;
  • menu: o menu está aberto, não ou sim.

O cursor do menu não precisa de uma variável própria: ele sempre fica em page.

O mapa inteiro como duas variáveis: page com os valores 0, 1 e 2, com 1 destacado, e menu com os valores não e sim, com não destacado

Etapa 1: a primeira tela ​

Desenhe a página do relógio ​

Comece um novo projeto como no episódio 1: + New Project, plataforma U8g2, tamanho de tela 128×64. Chamamos ele de "Pocket Gadget".

A primeira página tem um ícone e um título no topo, um relógio grande no meio e três pontos na parte inferior que mostram em qual página você está:

  1. O ícone. Abra a guia Imagens, busque "clock" e clique no ícone clock. De volta em Camadas, defina X 4 e Y 3.
  2. O título. Adicione um texto Clock na fonte profont12 em X 24, Y 14.
  3. O relógio. Adicione um texto 10:24 em profont29 em X 24, Y 46.
  4. Os pontos. Escolha a ferramenta elipse (o ícone de círculo) e desenhe três círculos pequenos com RX e RY 2, todos em Y 56: o primeiro em X 54 com Filled ativado, depois em X 62 e X 70 com Filled desativado.

Cada caractere da profont29 tem 16 pixels de largura, então "10:24" tem 80 pixels de largura, e (128 − 80) / 2 = 24 coloca ele no meio. No código, os pontos viram drawFilledEllipse(56, 58, 2, 2), drawEllipse(64, 58, 2, 2) e drawEllipse(72, 58, 2, 2): o inspetor mostra o canto superior esquerdo da caixa ao redor de um círculo, a U8g2 quer o centro dele. O ponto do meio fica em x = 64, o meio da tela.

Agrupe os pontos ​

No painel Camadas, clique em ellipse 6 e depois Shift-clique em ellipse 4 para selecionar os três pontos. Pressione Ctrl + G (Cmd + G em um Mac): os pontos agora são um grupo e se movem como uma peça. Clique duas vezes no nome do grupo e chame ele de dots.

Renomeie a tela: o nome vira a função ​

Clique duas vezes no nome da tela acima da tela de desenho, Screen 1, e renomeie ela para Clock. Esse nome vai direto para o código: a tela vira uma função chamada drawClock.

A página Clock no Lopaka: um ícone de relógio, o título Clock, 10:24 em uma fonte grande e três pontos, o primeiro preenchido; o painel Camadas mostra o grupo dots com ellipse 4, 5 e 6, e o código começa com void drawClock(void)

Este é o código da tela Clock:

cpp
// [BEGIN lopaka generated]
static const unsigned char image_clock_bits[] U8X8_PROGMEM = {0xe0,0x03,0x18,0x0c,0x94,0x14,0x82,0x20,0x86,0x30,0x81,0x40,0x81,0x40,0x87,0x70,0x01,0x41,0x01,0x42,0x06,0x34,0x02,0x20,0x94,0x14,0x98,0x0c,0xe0,0x03,0x00,0x00};
void drawClock(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // clock
    u8g2.drawXBMP(4, 3, 15, 16, image_clock_bits);
    // string 2
    u8g2.setFont(u8g2_font_profont12_tr);
    u8g2.drawStr(24, 14, "Clock");
    // string 3
    u8g2.setFont(u8g2_font_profont29_tr);
    u8g2.drawStr(24, 46, "10:24");
    // ellipse 4
    u8g2.drawFilledEllipse(56, 58, 2, 2);
    // ellipse 5
    u8g2.drawEllipse(64, 58, 2, 2);
    // ellipse 6
    u8g2.drawEllipse(72, 58, 2, 2);
}
// [END lopaka generated]

O grupo não aparece no código. Ele só ajuda você a mover os pontos juntos no editor.

Etapa 2: duplique as páginas ​

As outras páginas compartilham o mesmo layout, então não comece do zero. Selecione a tela Clock clicando no nome dela e clique em Duplicate no inspetor. Uma cópia chamada Clock copy aparece à direita. Renomeie ela para Weather e depois mude o que é diferente:

  • O ícone. Selecione a camada clock e pressione Delete. Em Imagens, busque "sun", adicione weather_sun e defina X 4, Y 3.
  • Os textos. Selecione string 2 e digite Weather. Selecione string 3, digite 23C e defina X como 40: três caracteres de 16 pixels fazem 48, e (128 − 48) / 2 = 40.
  • Os pontos. Abra o grupo dots, desative Filled para ellipse 4 e ative para ellipse 5.

A tela Weather selecionada no Lopaka: o ícone do sol, o título Weather, 23C e o segundo ponto preenchido; o inspetor mostra a tela em X 144, Y 0 com o botão Duplicate

Duplique mais uma vez para a frequência cardíaca: duplique Weather, renomeie a cópia para Heart, substitua o sol pelo ícone heart em X 4, Y 3, digite Heart e 72 bpm com X 16 e preencha o terceiro ponto em vez do segundo. Cada cópia recebe sua própria função: drawWeather() e drawHeart().

Etapa 3: o menu ​

Três linhas ​

O menu é uma tela nova. Clique em Adicionar tela no painel Camadas e renomeie a nova tela para Menu. Adicione três linhas, cada uma com um ícone e um rótulo:

LinhaÍcone em X 6Rótulo em profont12 em X 26
Clockclock, Y 3Clock, Y 15
Weatherweather_sun, Y 24Weather, Y 36
Heartheart, Y 45Heart, Y 57

Um texto novo começa na última fonte que você usou, então confira se cada rótulo está em profont12. As linhas estão a 21 pixels de distância: cada número na segunda linha é o primeiro mais 21, e a terceira linha adiciona 21 de novo.

Uma barra no modo XOR ​

Agora o cursor: uma barra preenchida por cima da primeira linha.

  1. Escolha a ferramenta retângulo, arraste um retângulo e defina X 2, Y 1, W 124, H 21.
  2. Ative Filled e Inverted (XOR).

A barra tem 21 pixels de altura, a altura de uma linha. No modo XOR, ela inverte os pixels sob ela, como no episódio 2: o ícone e o texto sob a barra ficam pretos. A barra é a camada superior, então ela é desenhada por último, sobre a linha que ela inverte.

Uma variável para a posição da barra ​

O sketch precisa mover a barra de linha em linha. Clique duas vezes no nome da camada do retângulo e renomeie ela para bar. Depois clique no pequeno botão Add variable na borda direita do campo Y no inspetor.

A tela Menu no Lopaka: a camada bar selecionada, X 2, Y 1, W 124, H 21, Filled e Inverted (XOR) ativados e o botão de variável em Y aceso; a primeira linha Clock está preta sobre a barra branca, e o código mostra int bar_y = 1

A posição Y da barra agora é uma variável, com o nome da camada:

cpp
// [BEGIN lopaka generated]
int bar_y = 1;
void drawMenu(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // clock
    u8g2.drawXBMP(6, 3, 15, 16, image_clock_bits);
    // weather_sun
    u8g2.drawXBMP(6, 24, 15, 16, image_weather_sun_bits);
    // heart
    u8g2.drawXBMP(6, 45, 15, 16, image_heart_bits);
    // string 22
    u8g2.setFont(u8g2_font_profont12_tr);
    u8g2.drawStr(26, 15, "Clock");
    // string 23
    u8g2.drawStr(26, 36, "Weather");
    // string 24
    u8g2.drawStr(26, 57, "Heart");
    // bar
    u8g2.setDrawColor(2);
    u8g2.drawBox(2, bar_y, 124, 21);
}
// [END lopaka generated]

Agora o código pode mover o cursor para qualquer linha: bar_y é 1 para a primeira linha, 22 para a segunda e 43 para a terceira. setDrawColor(2) é o modo XOR da U8g2. Os arrays de imagem acima de drawMenu foram deixados de fora aqui; mais sobre eles na Etapa 5.

Etapa 4: organize as telas como o mapa ​

As quatro telas moram em uma tela de desenho grande, então organize elas como o mapa: arraste a tela Menu pelo nome dela e solte ela sob Weather. Você também pode digitar a posição no inspetor: Menu em X 144, Y 100. Clique em Fit all à direita da tela de desenho para ver tudo de uma vez.

As quatro telas na tela de desenho do Lopaka: Clock, Weather e Heart em uma linha, Menu sob Weather em X 144, Y 100; o painel de código tem as guias Clock, Weather, Heart e Menu

Cada tela recebe sua própria guia no painel de código e sua própria função de desenho: drawClock(), drawWeather(), drawHeart() e drawMenu(). Onde uma tela fica na tela de desenho não muda o código dela.

Etapa 5: os botões e o sketch ​

Um botão puxa o pino para a terra ​

Agora os botões. Cada um conecta um pino da placa à terra: NEXT ao pino 2, OK ao pino 3. Com o pino definido como INPUT_PULLUP, um resistor dentro do chip mantém ele HIGH, e ele lê LOW enquanto o botão está pressionado.

O botão NEXT ligado entre o pino 2 e GND; o sinal no pino 2 cai de HIGH para LOW enquanto o botão está pressionado; abaixo dele o código óbvio, if (digitalRead(NEXT) == LOW) page++

Capture o pressionamento, não a retenção ​

O código óbvio diz: se o botão está abaixado, vá para a próxima página.

cpp
if (digitalRead(NEXT) == LOW) page++;

Mas loop() executa de novo e de novo, muitas vezes durante um pressionamento, e as páginas giram como uma máquina caça-níqueis.

Durante um pressionamento, o loop executa 50 vezes, e a página na telinha continua mudando

Botões reais também quicam: por cerca de um milissegundo, os contatos vibram, e um pressionamento pode parecer vários. Então não pergunte se o botão está abaixado. Capture o momento em que ele abaixa e ignore ele pelos próximos vinte milissegundos.

O sinal de um pressionamento com picos de bounce no começo; o código que confere se o botão está abaixado está riscado; uma seta marca o momento em que ele abaixa, seguida de uma zona hachurada, ignore 20 ms

É isso que a função pressed() no sketch faz:

cpp
// true only at the moment the button goes down; ignore bounce for 20 ms
bool pressed(int pin) {
  static bool was[4];
  static unsigned long t[4];
  bool down = digitalRead(pin) == LOW;
  bool press = down && !was[pin] && millis() - t[pin] > 20;
  if (down != was[pin]) t[pin] = millis();
  was[pin] = down;
  return press;
}

was lembra se o botão estava abaixado no loop anterior, e t lembra quando ele mudou por último. Um pressionamento só conta se o botão está abaixado agora, estava levantado antes e ficou quieto por mais de 20 ms. Os dois arrays são indexados pelo número do pino e têm quatro posições, o suficiente para os pinos 2 e 3. Se você ligar um botão ao pino 4 ou superior, torne eles maiores.

O sketch inteiro ​

As quatro funções de desenho do Lopaka vão onde o comentário diz; todo o resto no sketch é a lógica do menu:

cpp
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// Paste the code of the Clock, Weather, Heart and Menu tabs here

const int NEXT = 2, OK = 3;
int page = 0;          // 0 clock, 1 weather, 2 heart
bool menu = false;
bool changed = true;

// true only at the moment the button goes down; ignore bounce for 20 ms
bool pressed(int pin) {
  static bool was[4];
  static unsigned long t[4];
  bool down = digitalRead(pin) == LOW;
  bool press = down && !was[pin] && millis() - t[pin] > 20;
  if (down != was[pin]) t[pin] = millis();
  was[pin] = down;
  return press;
}

void setup() {
  pinMode(NEXT, INPUT_PULLUP);
  pinMode(OK, INPUT_PULLUP);
  u8g2.begin();
}

void loop() {
  if (pressed(NEXT)) { page = (page + 1) % 3; changed = true; }
  if (pressed(OK))   { menu = !menu;          changed = true; }
  if (!changed) return;
  changed = false;

  u8g2.clearBuffer();
  u8g2.setDrawColor(1);
  if (menu) {
    bar_y = 1 + page * 21;
    drawMenu();
  } else {
    switch (page) {
      case 0: drawClock(); break;
      case 1: drawWeather(); break;
      case 2: drawHeart(); break;
    }
  }
  u8g2.sendBuffer();
}

As duas variáveis do mapa estão no topo: page e menu. Em loop():

  • NEXT move para a próxima página. % 3 é o resto depois de dividir por 3, então depois da página 2 vem a página 0 de novo: da frequência cardíaca de volta ao relógio. No menu, a mesma linha move o cursor.
  • OK abre ou fecha o menu. O menu mostra a page atual, então o cursor espera na página de onde você veio, e OK no menu abre a página sob o cursor.
  • Depois desenhamos, mas só quando algo mudou. changed começa como true, então a primeira página é desenhada uma vez na inicialização.
  • No menu, a posição da barra vem do número da página: 1 + page * 21 dá 1, 22 ou 43, as três linhas da Etapa 3.
  • Caso contrário, um switch escolhe qual página desenhar.

O sketch inteiro ao lado de um modelo da tela mostrando a página Heart com page = 2 e menu = false; a linha if (!changed) return está destacada, e uma barra mostra send one frame, 44 ms

No simulador, enviar um quadro completo para este display leva cerca de 44 milissegundos; medimos sendBuffer() com micros(). Redesenhar em cada loop gastaria a maior parte do tempo enviando a mesma imagem de novo. Com changed, a tela redesenha só depois de um pressionamento, e loop() continua conferindo os botões.

drawMenu() deixa o modo XOR ativado, então o sketch volta ao desenho normal com setDrawColor(1) antes de cada quadro.

Copie cada guia para o sketch ​

O Copiar pega o código da guia aberta. Abra a guia Clock, clique em Copiar e cole o código no sketch, depois faça o mesmo para Weather e Heart.

Antes de copiar a guia Menu, desative Declare images nos interruptores ao lado do código. O menu usa os mesmos três ícones que as páginas, e as guias das páginas já declararam os arrays delas. O interruptor se aplica ao projeto inteiro, então ative ele de novo depois.

Compilado para um UNO, o sketch usa 14.588 bytes de flash (45%) e 1.619 bytes de RAM (79%). Como no episódio 1, a maior parte dessa RAM é o buffer de quadro de 1.024 bytes.

Execute ele no simulador ​

Abra o projeto do simulador do episódio 1: um Arduino UNO e um OLED SSD1306 ligados a GND, 5V, A5 (SCL) e A4 (SDA), com a biblioteca U8g2. Adicione dois botões de pressão e ligue eles:

  • NEXT: uma perna ao pino 2, a outra ao GND;
  • OK: uma perna ao pino 3, a outra ao GND.

Cole o sketch com as quatro guias no lugar e pressione Play. Next, next, next: as páginas giram e voltam ao relógio. OK abre o menu, bem na página atual. NEXT move o cursor para o clima, e OK abre ele. Um pressionamento, um passo.

O simulador executando o sketch: um Arduino UNO com o OLED e dois botões, NEXT no pino 2 e OK no pino 3; o display mostra o menu com a linha Weather destacada

Em uma placa real, ligue os botões do mesmo jeito. Um botão tátil comum tem quatro pernas unidas em pares; se um botão parecer pressionado o tempo todo ou nunca, use duas pernas em cantos opostos.

Recapitulação ​

  1. Cada tela do seu gadget é uma tela no Lopaka: cada uma vira sua própria função de desenho, e Duplicate mantém o layout.
  2. Desenhe o mapa de estados primeiro: duas variáveis, a página e se o menu está aberto, podem comandar o menu inteiro.
  3. Capture o pressionamento, não a retenção: reaja ao momento em que um botão abaixa e ignore ele por 20 ms depois disso.

Próximo episódio: Animations. As telas começam a se mover: de um GIF a um OLED minúsculo. Leia o episódio 7 ou assista no YouTube.

Pronto para testar? Abra o Lopaka e desenhe as telas do seu próprio gadget.