Lopaka Basics 06: pantallas y menús

Clic: la pantalla siguiente. Clic: y la siguiente. El otro botón abre un menú y el cursor espera en la página de la que viniste. Se siente como un dispositivo real, y son cuatro dibujos de Lopaka, dos botones y un sketch corto.
En este episodio descubrirás cómo funciona cada menú por dentro: un mapa de pantallas y botones que cabe en dos variables. Después dibujarás las cuatro pantallas en Lopaka, escribirás el sketch que lee los botones y lo ejecutarás en un Arduino UNO simulado.
Lo que vas a crear: un pequeño dispositivo de reloj, clima y ritmo cardíaco en un OLED de 128×64. El botón NEXT recorre tres páginas y OK abre un menú con un cursor. Lo que necesitas: un navegador y la configuración de simulador del episodio 1: un Arduino UNO, un OLED SSD1306 y la biblioteca U8g2. Dos pulsadores son nuevos. Una placa real es opcional; el cableado es el mismo que en el episodio 5.
Un menú es un mapa
El dispositivo siempre está en una pantalla
Cada menú en cada dispositivo funciona como un mapa. En cualquier momento el dispositivo muestra exactamente una pantalla. Qué pantalla es esa es el estado del dispositivo.
Cada botón es una flecha
Cada pulsación mueve el dispositivo de un estado al siguiente, como una flecha en el mapa. Nuestro dispositivo tiene tres páginas y un menú:
- NEXT recorre las páginas: el reloj, el clima, el ritmo cardíaco y de vuelta al reloj.
- OK salta al menú, con el cursor en la página de la que viniste.
- En el menú, NEXT mueve el cursor y OK abre esa página.

Dibuja primero el mapa y el código casi se escribe solo.
Dos variables guardan todo el mapa
Todo el mapa cabe en dos variables:
page: qué página, 0 para el reloj, 1 para el clima, 2 para el ritmo cardíaco;menu: si el menú está abierto, no o sí.
El cursor del menú no necesita una variable propia: siempre está en page.

Paso 1: la primera pantalla
Dibuja la página del reloj
Empieza un proyecto nuevo como en el episodio 1: + New Project, plataforma U8g2, tamaño de pantalla 128×64. Lo llamamos «Pocket Gadget».
La primera página tiene un ícono y un título arriba, un reloj grande en el centro y tres puntos abajo que muestran en qué página estás:
- El ícono. Abre la pestaña Images, busca «clock» y haz clic en el ícono
clock. De vuelta en Layers, pon X4e Y3. - El título. Añade un texto
Clocken la fuenteprofont12en X24, Y14. - El reloj. Añade un texto
10:24enprofont29en X24, Y46. - Los puntos. Elige la herramienta ellipse (el ícono de círculo) y dibuja tres círculos pequeños con RX y RY
2, todos en Y56: el primero en X54con Filled activado, después en X62y X70con Filled desactivado.
Cada carácter de profont29 tiene 16 píxeles de ancho, así que «10:24» mide 80 píxeles de ancho, y (128 − 80) / 2 = 24 lo pone en el centro. En el código los puntos se convierten en drawFilledEllipse(56, 58, 2, 2), drawEllipse(64, 58, 2, 2) y drawEllipse(72, 58, 2, 2): el inspector muestra la esquina superior izquierda de la caja alrededor de un círculo, U8g2 quiere su centro. El punto central está en x = 64, el centro de la pantalla.
Agrupa los puntos
En el panel Capas, haz clic en ellipse 6 y después Mayús-clic en ellipse 4 para seleccionar los tres puntos. Pulsa Ctrl + G (Cmd + G en Mac): los puntos ahora son un grupo y se mueven como una pieza. Haz doble clic en el nombre del grupo y llámalo dots.
Renombra la pantalla: el nombre se convierte en la función
Haz doble clic en el nombre de la pantalla sobre el lienzo, Screen 1, y renómbrala a Clock. Ese nombre va directo al código: la pantalla se convierte en una función llamada drawClock.

Este es el código de la pantalla 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]El grupo no aparece en el código. Solo te ayuda a mover los puntos juntos en el editor.
Paso 2: duplica las páginas
Las demás páginas comparten el mismo diseño, así que no empieces de cero. Selecciona la pantalla Clock haciendo clic en su nombre y haz clic en Duplicate en el inspector. Aparece una copia llamada Clock copy a la derecha. Renómbrala a Weather y después cambia lo que difiere:
- El ícono. Selecciona la capa
clocky pulsa Delete. En Images, busca «sun», añadeweather_suny pon X4, Y3. - Los textos. Selecciona
string 2y escribeWeather. Seleccionastring 3, escribe23Cy pon X en40: tres caracteres de 16 píxeles hacen 48, y (128 − 48) / 2 = 40. - Los puntos. Abre el grupo
dots, desactiva Filled paraellipse 4y actívalo paraellipse 5.

Duplica una vez más para el ritmo cardíaco: duplica Weather, renombra la copia a Heart, sustituye el sol por el ícono heart en X 4, Y 3, escribe Heart y 72 bpm con X 16, y rellena el tercer punto en lugar del segundo. Cada copia recibe su propia función: drawWeather() y drawHeart().
Paso 3: el menú
Tres filas
El menú es una pantalla nueva. Haz clic en Add Screen en el panel Capas y renombra la pantalla nueva a Menu. Añade tres filas, cada una con un ícono y una etiqueta:
| Row | Icon at X 6 | Label in profont12 at X 26 |
|---|---|---|
| Clock | clock, Y 3 | Clock, Y 15 |
| Weather | weather_sun, Y 24 | Weather, Y 36 |
| Heart | heart, Y 45 | Heart, Y 57 |
Un texto nuevo empieza con la última fuente que usaste, así que comprueba que cada etiqueta esté en profont12. Las filas están a 21 píxeles: cada número en la segunda fila es el primero más 21, y la tercera fila añade 21 otra vez.
Una barra en modo XOR
Ahora el cursor: una barra llena sobre la primera fila.
- Elige la herramienta rectangle, arrastra un rectángulo y pon X
2, Y1, W124, H21. - Activa Filled e Inverted (XOR).
La barra tiene 21 píxeles de alto, la altura de una fila. En modo XOR invierte los píxeles bajo ella, como en el episodio 2: el ícono y el texto bajo la barra se vuelven negros. La barra es la capa superior, así que se dibuja al final, sobre la fila que invierte.
Una variable para la posición de la barra
El sketch tiene que mover la barra de fila en fila. Haz doble clic en el nombre de capa del rectángulo y renómbrala a bar. Después haz clic en el pequeño botón Add variable en el borde derecho del campo Y en el inspector.

La posición Y de la barra ahora es una variable, con el nombre de la capa:
// [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]Ahora el código puede mover el cursor a cualquier fila: bar_y es 1 para la primera fila, 22 para la segunda y 43 para la tercera. setDrawColor(2) es el modo XOR de U8g2. Los arrays de imagen sobre drawMenu se omiten aquí; más sobre ellos en el Paso 5.
Paso 4: organiza las pantallas como el mapa
Las cuatro pantallas viven en un lienzo grande, así que organízalas como el mapa: arrastra la pantalla Menu por su nombre y suéltala bajo Weather. También puedes escribir la posición en el inspector: Menu en X 144, Y 100. Haz clic en Fit all a la derecha del lienzo para verlo todo a la vez.

Cada pantalla recibe su propia pestaña en el panel de código y su propia función de dibujo: drawClock(), drawWeather(), drawHeart() y drawMenu(). Dónde se sitúa una pantalla en el lienzo no cambia su código.
Paso 5: los botones y el sketch
Un botón lleva el pin a tierra
Ahora los botones. Cada uno conecta un pin de la placa a tierra: NEXT al pin 2, OK al pin 3. Con el pin en INPUT_PULLUP, una resistencia dentro del chip lo mantiene HIGH y lee LOW mientras el botón está pulsado.

Captura la pulsación, no la retención
El código obvio dice: si el botón está abajo, ve a la página siguiente.
if (digitalRead(NEXT) == LOW) page++;Pero loop() se ejecuta una y otra vez, muchas veces durante una pulsación, y las páginas giran como una tragaperras.

Los botones reales también rebotan: durante aproximadamente un milisegundo los contactos vibran y una pulsación puede parecer varias. Así que no preguntes si el botón está abajo. Captura el momento en que baja e ignóralo durante los siguientes veinte milisegundos.

Eso es lo que hace la función pressed() en el sketch:
// 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 recuerda si el botón estaba abajo en el loop anterior y t recuerda cuándo cambió por última vez. Una pulsación cuenta solo si el botón está abajo ahora, estaba arriba antes y ha estado tranquilo más de 20 ms. Ambos arrays se indexan por el número de pin y tienen cuatro huecos, suficientes para los pines 2 y 3. Si cableas un botón al pin 4 o superior, hazlos más grandes.
Todo el sketch
Las cuatro funciones de dibujo de Lopaka van donde dice el comentario; todo lo demás en el sketch es la lógica del menú:
#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();
}Las dos variables del mapa están arriba: page y menu. En loop():
- NEXT pasa a la página siguiente.
% 3es el resto tras dividir entre 3, así que tras la página 2 viene la página 0 otra vez: del ritmo cardíaco de vuelta al reloj. En el menú, la misma línea mueve el cursor. - OK abre o cierra el menú. El menú muestra la
pageactual, así que el cursor espera en la página de la que viniste, y OK en el menú abre la página bajo el cursor. - Después dibujamos, pero solo cuando algo cambió.
changedempieza comotrue, así que la primera página se dibuja una vez al arrancar. - En el menú, la posición de la barra viene del número de página:
1 + page * 21da 1, 22 o 43, las tres filas del Paso 3. - En caso contrario, un
switchelige qué página dibujar.

En el simulador, enviar un fotograma completo a esta pantalla lleva unos 44 milisegundos; medimos sendBuffer() con micros(). Redibujar en cada loop pasaría la mayor parte del tiempo enviando la misma imagen otra vez. Con changed, la pantalla se redibuja solo tras una pulsación y loop() sigue comprobando los botones.
drawMenu() deja el modo XOR activado, así que el sketch vuelve al dibujo normal con setDrawColor(1) antes de cada fotograma.
Copia cada pestaña en el sketch
Copy toma el código de la pestaña abierta. Abre la pestaña Clock, haz clic en Copy y pega el código en el sketch, después haz lo mismo para Weather y Heart.
Antes de copiar la pestaña Menu, desactiva Declare images en los interruptores junto al código. El menú usa los mismos tres íconos que las páginas y las pestañas de página ya han declarado sus arrays. El interruptor se aplica a todo el proyecto, así que vuelve a activarlo después.
Compilado para un UNO, el sketch usa 14.588 bytes de flash (45%) y 1.619 bytes de RAM (79%). Como en el episodio 1, la mayor parte de esa RAM es el búfer de fotogramas de 1.024 bytes.
Ejecútalo en el simulador
Abre el proyecto de simulador del episodio 1: un Arduino UNO y un OLED SSD1306 cableado a GND, 5V, A5 (SCL) y A4 (SDA), con la biblioteca U8g2. Añade dos pulsadores y cablealos:
- NEXT: una pata al pin 2, la otra a GND;
- OK: una pata al pin 3, la otra a GND.
Pega el sketch con las cuatro pestañas en su sitio y pulsa Play. Siguiente, siguiente, siguiente: las páginas dan la vuelta y vuelven al reloj. OK abre el menú, justo en la página actual. NEXT mueve el cursor al clima y OK lo abre. Una pulsación, un paso.

En una placa real, cablea los botones igual. Un pulsador táctil común tiene cuatro patas unidas en pares; si un botón parece siempre pulsado o nunca, usa dos patas en esquinas opuestas.
Resumen
- Cada pantalla de tu dispositivo es una pantalla en Lopaka: cada una se convierte en su propia función de dibujo y Duplicate mantiene el diseño.
- Dibuja primero el mapa de estados: dos variables, la página y si el menú está abierto, pueden ejecutar todo el menú.
- Captura la pulsación, no la retención: reacciona al momento en que un botón baja e ignóralo durante 20 ms después.
Próximo episodio: Animations. Las pantallas empiezan a moverse: de un GIF a un OLED diminuto. Lee el episodio 7 o míralo en YouTube.
¿Listo para probar? Abre Lopaka y dibuja las pantallas de tu propio dispositivo.