Lopaka Basics 06: telas e menus

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.

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.

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á:
- O ícone. Abra a guia Imagens, busque "clock" e clique no ícone
clock. De volta em Camadas, defina X4e Y3. - O título. Adicione um texto
Clockna fonteprofont12em X24, Y14. - O relógio. Adicione um texto
10:24emprofont29em X24, Y46. - 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 Y56: o primeiro em X54com Filled ativado, depois em X62e X70com 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.

Este é o código da tela Clock:
// [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
clocke pressione Delete. Em Imagens, busque "sun", adicioneweather_sune defina X4, Y3. - Os textos. Selecione
string 2e digiteWeather. Selecionestring 3, digite23Ce defina X como40: três caracteres de 16 pixels fazem 48, e (128 − 48) / 2 = 40. - Os pontos. Abra o grupo
dots, desative Filled paraellipse 4e ative paraellipse 5.

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 6 | Rótulo em profont12 em X 26 |
|---|---|---|
| Clock | clock, Y 3 | Clock, Y 15 |
| Weather | weather_sun, Y 24 | Weather, Y 36 |
| Heart | heart, Y 45 | Heart, 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.
- Escolha a ferramenta retângulo, arraste um retângulo e defina X
2, Y1, W124, H21. - 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 posição Y da barra agora é uma variável, com o nome da camada:
// [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.

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.

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.
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.

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.

É isso que a função pressed() no sketch faz:
// 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:
#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
pageatual, 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.
changedcomeça comotrue, 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 * 21dá 1, 22 ou 43, as três linhas da Etapa 3. - Caso contrário, um
switchescolhe qual página desenhar.

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.

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
- 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.
- Desenhe o mapa de estados primeiro: duas variáveis, a página e se o menu está aberto, podem comandar o menu inteiro.
- 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.