Ir al contenido

Lopaka Basics 06: pantallas y menús ​

Un módulo OLED SSD1306 simulado mostrando un menú con las filas Clock, Weather y Heart, la fila Weather resaltada, sobre dos botones etiquetados NEXT y OK

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.

El mapa de estados: Clock, Weather y Heart en una fila unidos por flechas NEXT, con NEXT desde Heart de vuelta a Clock; flechas OK bajan desde Weather al menú y vuelven; el menú tiene su propio NEXT; estás aquí marca Weather

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.

Todo el mapa como dos variables: page con los valores 0, 1 y 2, donde 1 está resaltado, y menu con los valores no y sí, donde no está resaltado

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:

  1. El ícono. Abre la pestaña Images, busca «clock» y haz clic en el ícono clock. De vuelta en Layers, pon X 4 e Y 3.
  2. El título. Añade un texto Clock en la fuente profont12 en X 24, Y 14.
  3. El reloj. Añade un texto 10:24 en profont29 en X 24, Y 46.
  4. 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 Y 56: el primero en X 54 con Filled activado, después en X 62 y X 70 con 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.

La página Clock en Lopaka: un ícono de reloj, el título Clock, 10:24 en una fuente grande y tres puntos, el primero relleno; el panel Capas muestra el grupo dots con ellipse 4, 5 y 6, y el código empieza con void drawClock(void)

Este es el código de la pantalla 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]

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 clock y pulsa Delete. En Images, busca «sun», añade weather_sun y pon X 4, Y 3.
  • Los textos. Selecciona string 2 y escribe Weather. Selecciona string 3, escribe 23C y pon X en 40: tres caracteres de 16 píxeles hacen 48, y (128 − 48) / 2 = 40.
  • Los puntos. Abre el grupo dots, desactiva Filled para ellipse 4 y actívalo para ellipse 5.

La pantalla Weather seleccionada en Lopaka: el ícono de sol, el título Weather, 23C y el segundo punto relleno; el inspector muestra la pantalla en X 144, Y 0 con el botón Duplicate

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:

RowIcon at X 6Label in profont12 at X 26
Clockclock, Y 3Clock, Y 15
Weatherweather_sun, Y 24Weather, Y 36
Heartheart, Y 45Heart, 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.

  1. Elige la herramienta rectangle, arrastra un rectángulo y pon X 2, Y 1, W 124, H 21.
  2. 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 pantalla Menu en Lopaka: la capa bar seleccionada, X 2, Y 1, W 124, H 21, Filled e Inverted (XOR) activados y el botón de variable en Y encendido; la primera fila Clock está en negro sobre la barra blanca, y el código muestra int bar_y = 1

La posición Y de la barra ahora es una variable, con el nombre de la capa:

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]

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.

Las cuatro pantallas en el lienzo de Lopaka: Clock, Weather y Heart en una fila, Menu bajo Weather en X 144, Y 100; el panel de código tiene las pestañas Clock, Weather, Heart y Menu

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.

El botón NEXT cableado entre el pin 2 y GND; la señal en el pin 2 cae de HIGH a LOW mientras el botón está pulsado; debajo, el código obvio, if (digitalRead(NEXT) == LOW) page++

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.

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

Durante una pulsación el loop se ejecuta 50 veces y la página en la pantalla pequeña no deja de cambiar

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.

La señal de una pulsación con picos de rebote al inicio; el código que comprueba si el botón está abajo está tachado; una flecha marca el momento en que baja, seguida de una zona rayada, ignorar 20 ms

Eso es lo que hace la función pressed() en el sketch:

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 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ú:

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();
}

Las dos variables del mapa están arriba: page y menu. En loop():

  • NEXT pasa a la página siguiente. % 3 es 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 page actual, 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ó. changed empieza como true, 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 * 21 da 1, 22 o 43, las tres filas del Paso 3.
  • En caso contrario, un switch elige qué página dibujar.

Todo el sketch junto a un modelo de la pantalla mostrando la página Heart con page = 2 y menu = false; la línea if (!changed) return está resaltada, y una barra dice enviar un fotograma, 44 ms

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.

El simulador ejecutando el sketch: un Arduino UNO con el OLED y dos botones, NEXT en el pin 2 y OK en el pin 3; la pantalla muestra el menú con la fila Weather resaltada

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 ​

  1. 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.
  2. Dibuja primero el mapa de estados: dos variables, la página y si el menú está abierto, pueden ejecutar todo el menú.
  3. 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.