Zum Inhalt springen

Lopaka Basics 06: Screens & Menüs ​

Ein simuliertes SSD1306-OLED-Modul, das ein Menü mit den Zeilen Clock, Weather und Heart zeigt, die Weather-Zeile hervorgehoben, über zwei Buttons mit den Beschriftungen NEXT und OK

Klick: der nächste Screen. Klick: und der nächste. Der andere Button öffnet ein Menü, und der Cursor wartet auf der Seite, von der du kamst. Es fühlt sich an wie ein echtes Gadget, und es sind vier Zeichnungen aus Lopaka, zwei Buttons und ein kurzer Sketch.

In dieser Episode findest du heraus, wie jedes Menü innen funktioniert: eine Karte aus Screens und Buttons, die in zwei Variablen passt. Dann zeichnest du die vier Screens in Lopaka, schreibst den Sketch, der die Buttons liest, und führst ihn auf einem simulierten Arduino UNO aus.

Was du bauen wirst: ein kleines Uhr-, Wetter- und Herzfrequenz-Gadget auf einem 128×64-OLED. Der NEXT-Button blättert durch drei Seiten, und OK öffnet ein Menü mit einem Cursor. Was du brauchst: einen Browser und die Simulator-Einrichtung aus Episode 1: ein Arduino UNO, ein SSD1306-OLED und die U8g2-Bibliothek. Zwei Drucktaster sind neu. Ein echtes Board ist optional; die Verdrahtung ist dieselbe wie in Episode 5.

Ein Menü ist eine Karte ​

Das Gerät ist immer auf einem Screen ​

Jedes Menü in jedem Gadget funktioniert wie eine Karte. In jedem Moment zeigt das Gerät genau einen Screen. Welcher Screen das ist, ist der Zustand des Geräts.

Jeder Button ist ein Pfeil ​

Jeder Tastendruck bewegt das Gerät von einem Zustand zum nächsten, wie ein Pfeil auf der Karte. Unser Gadget hat drei Seiten und ein Menü:

  • NEXT geht durch die Seiten: Uhr, Wetter, Herzfrequenz und zurück zur Uhr.
  • OK springt nach unten ins Menü, mit dem Cursor auf der Seite, von der du kamst.
  • Im Menü bewegt NEXT den Cursor, und OK öffnet diese Seite.

Die Karte der Zustände: Clock, Weather und Heart in einer Reihe, verbunden durch NEXT-Pfeile, mit NEXT von Heart zurück zu Clock; OK-Pfeile führen von Weather nach unten zum Menü und zurück; das Menü hat sein eigenes NEXT; du bist hier markiert Weather

Zeichne zuerst die Karte, und der Code schreibt sich fast von selbst.

Zwei Variablen halten die ganze Karte ​

Die ganze Karte passt in zwei Variablen:

  • page: welche Seite, 0 für die Uhr, 1 für das Wetter, 2 für die Herzfrequenz;
  • menu: ist das Menü offen, nein oder ja.

Der Menü-Cursor braucht keine eigene Variable: Er steht immer auf page.

Die ganze Karte als zwei Variablen: page mit den Werten 0, 1 und 2, wobei 1 hervorgehoben ist, und menu mit den Werten nein und ja, wobei nein hervorgehoben ist

Schritt 1: Der erste Screen ​

Zeichne die Uhrseite ​

Starte ein neues Projekt wie in Episode 1: + Neues Projekt, Plattform U8g2, Screen-Größe 128×64. Wir nannten es „Pocket Gadget“.

Die erste Seite hat ein Icon und einen Titel oben, eine große Uhr in der Mitte und drei Punkte unten, die zeigen, auf welcher Seite du bist:

  1. Das Icon. Öffne den Tab Bilder, suche nach „clock“ und klicke das clock-Icon. Setze zurück in Ebenen X 4 und Y 3.
  2. Der Titel. Füge einen Text Clock in der Schriftart profont12 bei X 24, Y 14 hinzu.
  3. Die Uhr. Füge einen Text 10:24 in profont29 bei X 24, Y 46 hinzu.
  4. Die Punkte. Wähle das Ellipsen-Werkzeug (das Kreis-Icon) und zeichne drei kleine Kreise mit RX und RY 2, alle bei Y 56: den ersten bei X 54 mit eingeschaltetem Gefüllt, dann bei X 62 und X 70 mit ausgeschaltetem Gefüllt.

Jedes Zeichen von profont29 ist 16 Pixel breit, also ist „10:24“ 80 Pixel breit, und (128 − 80) / 2 = 24 legt es in die Mitte. Im Code werden die Punkte zu drawFilledEllipse(56, 58, 2, 2), drawEllipse(64, 58, 2, 2) und drawEllipse(72, 58, 2, 2): Der Inspektor zeigt die obere linke Ecke der Box um einen Kreis, U8g2 will seinen Mittelpunkt. Der mittlere Punkt sitzt bei x = 64, der Mitte des Screens.

Gruppiere die Punkte ​

Klicke im Ebenen-Panel auf ellipse 6, dann Shift-klicke auf ellipse 4, um alle drei Punkte auszuwählen. Drücke Strg + G (Cmd + G auf einem Mac): Die Punkte sind jetzt eine Gruppe und bewegen sich als ein Stück. Doppelklicke auf den Namen der Gruppe und nenne sie dots.

Benenne den Screen um: Der Name wird zur Funktion ​

Doppelklicke auf den Namen des Screens über der Zeichenfläche, Screen 1, und benenne ihn in Clock um. Dieser Name geht direkt in den Code: Der Screen wird zu einer Funktion namens drawClock.

Die Clock-Seite in Lopaka: ein Uhr-Icon, der Titel Clock, 10:24 in großer Schriftart und drei Punkte, der erste gefüllt; das Ebenen-Panel zeigt die dots-Gruppe mit ellipse 4, 5 und 6, und der Code beginnt mit void drawClock(void)

Das ist der Code des Clock-Screens:

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]

Die Gruppe erscheint nicht im Code. Sie hilft dir nur, die Punkte gemeinsam im Editor zu bewegen.

Schritt 2: Dupliziere die Seiten ​

Die anderen Seiten teilen sich dasselbe Layout, also fange nicht von vorne an. Wähle den Clock-Screen aus, indem du seinen Namen klickst, und klicke im Inspektor auf Duplizieren. Eine Kopie namens Clock copy erscheint rechts. Benenne sie in Weather um und ändere dann, was sich unterscheidet:

  • Das Icon. Wähle die Ebene clock aus und drücke Löschen. Suche in Bilder nach „sun“, füge weather_sun hinzu und setze X 4, Y 3.
  • Die Texte. Wähle string 2 aus und tippe Weather. Wähle string 3 aus, tippe 23C und setze X auf 40: Drei Zeichen mit 16 Pixeln machen 48, und (128 − 48) / 2 = 40.
  • Die Punkte. Öffne die Gruppe dots, schalte Gefüllt für ellipse 4 aus und für ellipse 5 ein.

Der Weather-Screen in Lopaka ausgewählt: das Sonnen-Icon, der Titel Weather, 23C und der zweite Punkt gefüllt; der Inspektor zeigt den Screen bei X 144, Y 0 mit dem Duplizieren-Button

Dupliziere noch einmal für die Herzfrequenz: Dupliziere Weather, benenne die Kopie in Heart um, ersetze die Sonne durch das heart-Icon bei X 4, Y 3, tippe Heart und 72 bpm mit X 16 und fülle den dritten Punkt statt des zweiten. Jede Kopie bekommt ihre eigene Funktion: drawWeather() und drawHeart().

Schritt 3: Das Menü ​

Drei Zeilen ​

Das Menü ist ein neuer Screen. Klicke im Ebenen-Panel auf Screen hinzufügen und benenne den neuen Screen in Menu um. Füge drei Zeilen hinzu, jede mit einem Icon und einer Beschriftung:

ZeileIcon bei X 6Beschriftung in profont12 bei X 26
Clockclock, Y 3Clock, Y 15
Weatherweather_sun, Y 24Weather, Y 36
Heartheart, Y 45Heart, Y 57

Ein neuer Text startet in der letzten Schriftart, die du genutzt hast, also prüfe, dass jede Beschriftung in profont12 ist. Die Zeilen sind 21 Pixel auseinander: Jede Zahl in der zweiten Zeile ist die erste plus 21, und die dritte Zeile addiert wieder 21.

Eine Leiste im XOR-Modus ​

Jetzt der Cursor: eine gefüllte Leiste über der ersten Zeile.

  1. Wähle das Rechteck-Werkzeug, ziehe ein Rechteck und setze X 2, Y 1, W 124, H 21.
  2. Schalte Gefüllt und Invertiert (XOR) ein.

Die Leiste ist 21 Pixel hoch, die Höhe einer Zeile. Im XOR-Modus kippt sie die Pixel darunter, wie in Episode 2: Das Icon und der Text unter der Leiste werden schwarz. Die Leiste ist die oberste Ebene, also wird sie zuletzt gezeichnet, über der Zeile, die sie kippt.

Eine Variable für die Position der Leiste ​

Der Sketch muss die Leiste von Zeile zu Zeile bewegen. Doppelklicke auf den Ebenennamen des Rechtecks und benenne ihn in bar um. Klicke dann den kleinen Button Variable hinzufügen am rechten Rand des Y-Feldes im Inspektor.

Der Menu-Screen in Lopaka: Die bar-Ebene ausgewählt, X 2, Y 1, W 124, H 21, Gefüllt und Invertiert (XOR) an und der Variablen-Button bei Y leuchtend; die erste Zeile Clock ist schwarz auf der weißen Leiste, und der Code zeigt int bar_y = 1

Die Y-Position der Leiste ist jetzt eine Variable, benannt nach der Ebene:

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]

Jetzt kann der Code den Cursor in jede Zeile bewegen: bar_y ist 1 für die erste Zeile, 22 für die zweite und 43 für die dritte. setDrawColor(2) ist der XOR-Modus von U8g2. Die Bild-Arrays über drawMenu sind hier ausgelassen; mehr dazu in Schritt 5.

Schritt 4: Ordne die Screens wie die Karte an ​

Alle vier Screens leben auf einer großen Zeichenfläche, also ordne sie wie die Karte an: Ziehe den Menu-Screen an seinem Namen und lege ihn unter Weather. Du kannst die Position auch in den Inspektor tippen: Menu bei X 144, Y 100. Klicke rechts auf der Zeichenfläche auf Alle einpassen, um alles auf einmal zu sehen.

Alle vier Screens auf der Lopaka-Zeichenfläche: Clock, Weather und Heart in einer Reihe, Menu unter Weather bei X 144, Y 100; das Code-Panel hat die Tabs Clock, Weather, Heart und Menu

Jeder Screen bekommt seinen eigenen Tab im Code-Panel und seine eigene Zeichenfunktion: drawClock(), drawWeather(), drawHeart() und drawMenu(). Wo ein Screen auf der Zeichenfläche sitzt, ändert seinen Code nicht.

Schritt 5: Die Buttons und der Sketch ​

Ein Button zieht den Pin auf Masse ​

Jetzt die Buttons. Jeder verbindet einen Pin des Boards mit Masse: NEXT mit Pin 2, OK mit Pin 3. Mit dem Pin auf INPUT_PULLUP gesetzt hält ein Widerstand im Chip ihn HIGH, und er liest LOW, während der Button gedrückt ist.

Der NEXT-Button, verdrahtet zwischen Pin 2 und GND; das Signal an Pin 2 fällt von HIGH zu LOW, während der Button gedrückt ist; darunter der offensichtliche Code, if (digitalRead(NEXT) == LOW) page++

Fange den Druck, nicht das Halten ​

Der offensichtliche Code sagt: Wenn der Button unten ist, gehe zur nächsten Seite.

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

Aber loop() läuft wieder und wieder, viele Male während eines Drucks, und die Seiten wirbeln wie ein Spielautomat.

Während eines Drucks läuft die Schleife 50 Mal, und die Seite auf dem kleinen Screen wechselt ständig

Echte Buttons prellen auch: Für etwa eine Millisekunde flattern die Kontakte, und ein Druck kann wie mehrere aussehen. Also frage nicht, ob der Button unten ist. Fange den Moment, in dem er nach unten geht, und ignoriere ihn für die nächsten zwanzig Millisekunden.

Das Signal eines Drucks mit Prellspitzen am Anfang; der Code, der prüft, ob der Button unten ist, ist durchgestrichen; ein Pfeil markiert den Moment, in dem er nach unten geht, gefolgt von einer schraffierten Zone, ignoriere 20 ms

Das tut die pressed()-Funktion im 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 merkt sich, ob der Button in der vorherigen Schleife unten war, und t merkt sich, wann er sich zuletzt änderte. Ein Druck zählt nur, wenn der Button jetzt unten ist, vorher oben war und seit mehr als 20 ms ruhig ist. Beide Arrays sind nach Pinnummer indiziert und haben vier Slots, genug für Pins 2 und 3. Wenn du einen Button an Pin 4 oder höher verdrahtest, mache sie größer.

Der ganze Sketch ​

Die vier Zeichenfunktionen aus Lopaka kommen dorthin, wo der Kommentar sagt; alles andere im Sketch ist die Menülogik:

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

Die zwei Variablen aus der Karte sind oben: page und menu. In loop():

  • NEXT bewegt zur nächsten Seite. % 3 ist der Rest nach dem Teilen durch 3, also kommt nach Seite 2 wieder Seite 0: von der Herzfrequenz zurück zur Uhr. Im Menü bewegt dieselbe Zeile den Cursor.
  • OK öffnet oder schließt das Menü. Das Menü zeigt die aktuelle page, also wartet der Cursor auf der Seite, von der du kamst, und OK im Menü öffnet die Seite unter dem Cursor.
  • Dann zeichnen wir, aber nur, wenn sich etwas geändert hat. changed startet als true, also wird die erste Seite beim Start einmal gezeichnet.
  • Im Menü kommt die Position der Leiste von der Seitenzahl: 1 + page * 21 ergibt 1, 22 oder 43, die drei Zeilen aus Schritt 3.
  • Sonst wählt ein switch, welche Seite gezeichnet wird.

Der ganze Sketch neben einem Modell des Screens, das die Heart-Seite mit page = 2 und menu = false zeigt; die Zeile if (!changed) return ist hervorgehoben, und eine Leiste liest sende einen Frame, 44 ms

Im Simulator braucht das Senden eines vollen Frames an dieses Display etwa 44 Millisekunden; wir maßen sendBuffer() mit micros(). Bei jeder Schleife neu zu zeichnen, würde die meiste Zeit damit verbringen, dasselbe Bild erneut zu senden. Mit changed zeichnet der Screen nur nach einem Druck neu, und loop() prüft weiter die Buttons.

drawMenu() lässt den XOR-Modus an, also wechselt der Sketch mit setDrawColor(1) vor jedem Frame zurück zum normalen Zeichnen.

Kopiere jeden Tab in den Sketch ​

Kopieren nimmt den Code des offenen Tabs. Öffne den Tab Clock, klicke auf Kopieren und füge den Code in den Sketch ein, dann mache dasselbe für Weather und Heart.

Bevor du den Tab Menu kopierst, schalte Bilder deklarieren in den Schaltern neben dem Code aus. Das Menü nutzt dieselben drei Icons wie die Seiten, und die Seiten-Tabs haben ihre Arrays bereits deklariert. Der Schalter gilt für das ganze Projekt, also schalte ihn danach wieder ein.

Für einen UNO kompiliert, nutzt der Sketch 14.588 Bytes Flash (45 %) und 1.619 Bytes RAM (79 %). Wie in Episode 1 ist der größte Teil dieses RAM der 1.024-Byte-Frame-Puffer.

Führe ihn im Simulator aus ​

Öffne das Simulatorprojekt aus Episode 1: ein Arduino UNO und ein SSD1306-OLED, verdrahtet mit GND, 5V, A5 (SCL) und A4 (SDA), mit der U8g2-Bibliothek. Füge zwei Drucktaster hinzu und verdrahte sie:

  • NEXT: ein Bein zu Pin 2, das andere zu GND;
  • OK: ein Bein zu Pin 3, das andere zu GND.

Füge den Sketch mit den vier Tabs an Ort und Stelle ein und drücke Play. Weiter, weiter, weiter: Die Seiten gehen rund und zurück zur Uhr. OK öffnet das Menü, genau auf der aktuellen Seite. NEXT bewegt den Cursor zum Wetter, und OK öffnet es. Ein Druck, ein Schritt.

Der Simulator, der den Sketch ausführt: ein Arduino UNO mit dem OLED und zwei Buttons, NEXT auf Pin 2 und OK auf Pin 3; das Display zeigt das Menü mit hervorgehobener Weather-Zeile

Verdrahte die Buttons auf einem echten Board auf dieselbe Weise. Ein gängiger taktiler Button hat vier Beine, paarweise verbunden; wenn ein Button ständig oder nie gedrückt zu sein scheint, nutze zwei Beine an gegenüberliegenden Ecken.

Zusammenfassung ​

  1. Jeder Screen deines Gadgets ist ein Screen in Lopaka: Jeder wird zu seiner eigenen Zeichenfunktion, und Duplizieren behält das Layout.
  2. Zeichne zuerst die Karte der Zustände: Zwei Variablen, die Seite und ob das Menü offen ist, können das ganze Menü betreiben.
  3. Fange den Druck, nicht das Halten: Reagiere auf den Moment, in dem ein Button nach unten geht, und ignoriere ihn danach 20 ms lang.

Nächste Episode: Animationen. Die Screens beginnen sich zu bewegen: von einem GIF zu einem winzigen OLED. Lies Episode 7 oder schaue sie auf YouTube.

Bereit, es zu versuchen? Öffne Lopaka und zeichne die Screens deines eigenen Gadgets.