Przejdź do treści

Lopaka Basics 06: Ekrany i menu ​

Symulowany moduł OLED SSD1306 pokazujący menu z wierszami Clock, Weather i Heart, z podświetlonym wierszem Weather, nad dwoma przyciskami oznaczonymi NEXT i OK

Klik: następny ekran. Klik: i kolejny. Drugi przycisk otwiera menu, a kursor czeka na stronie, z której przyszedłeś. To jak prawdziwy gadżet, a są to cztery rysunki z Lopaka, dwa przyciski i jeden krótki szkic.

W tym odcinku dowiesz się, jak działa każde menu od środka: mapa ekranów i przycisków, która mieści się w dwóch zmiennych. Następnie narysujesz cztery ekrany w Lopaka, napiszesz szkic odczytujący przyciski i uruchomisz go na symulowanym Arduino UNO.

Co zbudujesz: mały gadżet z zegarem, pogodą i tętnem na OLED 128×64. Przycisk NEXT przewija trzy strony, a OK otwiera menu z kursorem. Czego potrzebujesz: przeglądarki i konfiguracji symulatora z odcinka 1: Arduino UNO, OLED SSD1306 i biblioteki U8g2. Dwa przyciski są nowe. Prawdziwa płytka jest opcjonalna; okablowanie jest takie samo jak w odcinku 5.

Menu to mapa ​

Urządzenie zawsze jest na jednym ekranie ​

Każde menu w każdym gadżecie działa jak mapa. W każdej chwili urządzenie pokazuje dokładnie jeden ekran. To, który ekran, jest stanem urządzenia.

Każdy przycisk to strzałka ​

Każde naciśnięcie przycisku przenosi urządzenie z jednego stanu do następnego, jak strzałka na mapie. Nasz gadżet ma trzy strony i menu:

  • NEXT przewija strony: zegar, pogodę, tętno i z powrotem do zegara.
  • OK przechodzi w dół do menu, z kursorem na stronie, z której przyszedłeś.
  • W menu NEXT przesuwa kursor, a OK otwiera tę stronę.

Mapa stanów: Clock, Weather i Heart w rzędzie połączone strzałkami NEXT, z NEXT od Heart z powrotem do Clock; strzałki OK prowadzą w dół z Weather do menu i z powrotem; menu ma własne NEXT; jesteś tutaj oznacza Weather

Najpierw narysuj mapę, a kod prawie napisze się sam.

Dwie zmienne przechowują całą mapę ​

Cała mapa mieści się w dwóch zmiennych:

  • page: która strona, 0 dla zegara, 1 dla pogody, 2 dla tętna;
  • menu: czy menu jest otwarte, nie lub tak.

Kursor menu nie potrzebuje własnej zmiennej: zawsze stoi na page.

Cała mapa jako dwie zmienne: page z wartościami 0, 1 i 2, gdzie 1 jest podświetlone, oraz menu z wartościami nie i tak, gdzie nie jest podświetlone

Krok 1: pierwszy ekran ​

Narysuj stronę zegara ​

Rozpocznij nowy projekt jak w odcinku 1: + Nowy projekt, platforma U8g2, rozmiar ekranu 128×64. Nazwaliśmy go „Pocket Gadget”.

Pierwsza strona ma ikonę i tytuł u góry, duży zegar pośrodku oraz trzy kropki na dole pokazujące, na której stronie jesteś:

  1. Ikona. Otwórz kartę Obrazy, wyszukaj „clock” i kliknij ikonę clock. Z powrotem w Warstwach ustaw X 4 i Y 3.
  2. Tytuł. Dodaj tekst Clock czcionką profont12 na X 24, Y 14.
  3. Zegar. Dodaj tekst 10:24 czcionką profont29 na X 24, Y 46.
  4. Kropki. Wybierz narzędzie elipsa (ikona koła) i narysuj trzy małe koła z RX i RY 2, wszystkie na Y 56: pierwsze na X 54 z włączoną opcją Wypełniony, następnie na X 62 i X 70 z wyłączoną opcją Wypełniony.

Każdy znak profont29 ma 16 pikseli szerokości, więc „10:24” ma 80 pikseli szerokości, a (128 − 80) / 2 = 24 umieszcza je pośrodku. W kodzie kropki stają się drawFilledEllipse(56, 58, 2, 2), drawEllipse(64, 58, 2, 2) i drawEllipse(72, 58, 2, 2): inspektor pokazuje lewy górny róg ramki wokół koła, U8g2 chce jego środka. Środkowa kropka znajduje się na x = 64, pośrodku ekranu.

Zgrupuj kropki ​

W panelu Warstwy kliknij ellipse 6, a następnie kliknij z Shift ellipse 4, aby zaznaczyć wszystkie trzy kropki. Naciśnij Ctrl + G (Cmd + G na Macu): kropki są teraz grupą i poruszają się jako jeden element. Kliknij dwukrotnie nazwę grupy i nazwij ją dots.

Zmień nazwę ekranu: nazwa staje się funkcją ​

Kliknij dwukrotnie nazwę ekranu nad obszarem rysowania, Screen 1, i zmień ją na Clock. Ta nazwa trafia wprost do kodu: ekran staje się funkcją o nazwie drawClock.

Strona Clock w Lopaka: ikona zegara, tytuł Clock, 10:24 dużą czcionką i trzy kropki, pierwsza wypełniona; panel Warstwy pokazuje grupę dots z ellipse 4, 5 i 6, a kod zaczyna się od void drawClock(void)

Oto kod ekranu 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]

Grupa nie występuje w kodzie. Pomaga ci tylko przesuwać kropki razem w edytorze.

Krok 2: zduplikuj strony ​

Pozostałe strony mają ten sam układ, więc nie zaczynaj od nowa. Zaznacz ekran Clock, klikając jego nazwę, i kliknij Duplikuj w inspektorze. Po prawej pojawia się kopia o nazwie Clock copy. Zmień jej nazwę na Weather, a następnie zmień to, co się różni:

  • Ikona. Zaznacz warstwę clock i naciśnij Delete. W Obrazach wyszukaj „sun”, dodaj weather_sun i ustaw X 4, Y 3.
  • Teksty. Zaznacz string 2 i wpisz Weather. Zaznacz string 3, wpisz 23C i ustaw X na 40: trzy znaki po 16 pikseli dają 48, a (128 − 48) / 2 = 40.
  • Kropki. Otwórz grupę dots, wyłącz Wypełniony dla ellipse 4 i włącz dla ellipse 5.

Ekran Weather zaznaczony w Lopaka: ikona słońca, tytuł Weather, 23C i druga kropka wypełniona; inspektor pokazuje ekran na X 144, Y 0 z przyciskiem Duplikuj

Zduplikuj jeszcze raz dla tętna: zduplikuj Weather, zmień nazwę kopii na Heart, zamień słońce na ikonę heart na X 4, Y 3, wpisz Heart i 72 bpm z X 16 oraz wypełnij trzecią kropkę zamiast drugiej. Każda kopia otrzymuje własną funkcję: drawWeather() i drawHeart().

Krok 3: menu ​

Trzy wiersze ​

Menu to nowy ekran. Kliknij Dodaj ekran w panelu Warstwy i zmień nazwę nowego ekranu na Menu. Dodaj trzy wiersze, każdy z ikoną i etykietą:

WierszIkona na X 6Etykieta czcionką profont12 na X 26
Clockclock, Y 3Clock, Y 15
Weatherweather_sun, Y 24Weather, Y 36
Heartheart, Y 45Heart, Y 57

Nowy tekst zaczyna się od ostatniej użytej czcionki, więc sprawdź, czy każda etykieta jest czcionką profont12. Wiersze są oddalone o 21 pikseli: każda liczba w drugim wierszu to pierwsza plus 21, a trzeci wiersz dodaje kolejne 21.

Pasek w trybie XOR ​

Teraz kursor: wypełniony pasek na pierwszym wierszu.

  1. Wybierz narzędzie prostokąt, przeciągnij prostokąt i ustaw X 2, Y 1, W 124, H 21.
  2. Włącz Wypełniony i Odwrócony (XOR).

Pasek ma 21 pikseli wysokości, wysokość jednego wiersza. W trybie XOR odwraca piksele pod sobą, jak w odcinku 2: ikona i tekst pod paskiem stają się czarne. Pasek jest warstwą na wierzchu, więc jest rysowany jako ostatni, nad wierszem, który odwraca.

Zmienna dla pozycji paska ​

Szkic musi przesuwać pasek z wiersza na wiersz. Kliknij dwukrotnie nazwę warstwy prostokąta i zmień ją na bar. Następnie kliknij mały przycisk Dodaj zmienną na prawej krawędzi pola Y w inspektorze.

Ekran Menu w Lopaka: warstwa bar zaznaczona, X 2, Y 1, W 124, H 21, włączone opcje Wypełniony i Odwrócony (XOR) oraz przycisk zmiennej na Y podświetlony; pierwszy wiersz Clock jest czarny na białym pasku, a kod pokazuje int bar_y = 1

Pozycja Y paska jest teraz zmienną, nazwaną od warstwy:

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]

Teraz kod może przesunąć kursor do dowolnego wiersza: bar_y to 1 dla pierwszego wiersza, 22 dla drugiego i 43 dla trzeciego. setDrawColor(2) to tryb XOR w U8g2. Tablice obrazów nad drawMenu są tutaj pominięte; więcej o nich w Kroku 5.

Krok 4: ułóż ekrany jak mapę ​

Wszystkie cztery ekrany żyją na jednym dużym obszarze rysowania, więc ułóż je jak mapę: przeciągnij ekran Menu za jego nazwę i upuść go pod Weather. Możesz też wpisać pozycję do inspektora: Menu na X 144, Y 100. Kliknij Dopasuj wszystko po prawej stronie obszaru rysowania, aby zobaczyć wszystko naraz.

Wszystkie cztery ekrany na obszarze rysowania Lopaka: Clock, Weather i Heart w rzędzie, Menu pod Weather na X 144, Y 100; panel kodu ma karty Clock, Weather, Heart i Menu

Każdy ekran otrzymuje własną kartę w panelu kodu i własną funkcję rysowania: drawClock(), drawWeather(), drawHeart() i drawMenu(). To, gdzie ekran znajduje się na obszarze rysowania, nie zmienia jego kodu.

Krok 5: przyciski i szkic ​

Przycisk ściąga pin do masy ​

Teraz przyciski. Każdy z nich łączy pin płytki z masą: NEXT do pinu 2, OK do pinu 3. Przy pinie ustawionym na INPUT_PULLUP rezystor wewnątrz chipa utrzymuje go w stanie HIGH, a odczytuje LOW, gdy przycisk jest naciśnięty.

Przycisk NEXT podłączony między pinem 2 a GND; sygnał na pinie 2 spada z HIGH do LOW, gdy przycisk jest naciśnięty; poniżej oczywisty kod, if (digitalRead(NEXT) == LOW) page++

Wychwyć naciśnięcie, nie przytrzymanie ​

Oczywisty kod mówi: jeśli przycisk jest wciśnięty, przejdź do następnej strony.

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

Ale loop() działa w kółko, wiele razy podczas jednego naciśnięcia, a strony wirują jak jednoręki bandyta.

Podczas jednego naciśnięcia pętla działa 50 razy, a strona na małym ekranie ciągle się zmienia

Prawdziwe przyciski też drgają: przez około milisekundę styki trzaskają, a jedno naciśnięcie może wyglądać jak kilka. Więc nie pytaj, czy przycisk jest wciśnięty. Wychwyć moment, w którym się wciska, i ignoruj go przez następne dwadzieścia milisekund.

Sygnał jednego naciśnięcia ze skokami drgań na początku; kod sprawdzający, czy przycisk jest wciśnięty, jest przekreślony; strzałka oznacza moment, w którym się wciska, a po niej zakreskowana strefa, ignoruj 20 ms

To właśnie robi funkcja pressed() w szkicu:

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 pamięta, czy przycisk był wciśnięty w poprzedniej pętli, a t pamięta, kiedy ostatnio się zmienił. Naciśnięcie liczy się tylko wtedy, gdy przycisk jest teraz wciśnięty, wcześniej był zwolniony i był cichy przez ponad 20 ms. Obie tablice są indeksowane numerem pinu i mają cztery sloty, wystarczające dla pinów 2 i 3. Jeśli podłączysz przycisk do pinu 4 lub wyższego, powiększ je.

Cały szkic ​

Cztery funkcje rysowania z Lopaka trafiają tam, gdzie mówi komentarz; wszystko inne w szkicu to logika 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();
}

Dwie zmienne z mapy są na górze: page i menu. W loop():

  • NEXT przechodzi do następnej strony. % 3 to reszta z dzielenia przez 3, więc po stronie 2 znów następuje strona 0: od tętna z powrotem do zegara. W menu ta sama linia przesuwa kursor.
  • OK otwiera lub zamyka menu. Menu pokazuje bieżącą page, więc kursor czeka na stronie, z której przyszedłeś, a OK w menu otwiera stronę pod kursorem.
  • Następnie rysujemy, ale tylko wtedy, gdy coś się zmieniło. changed zaczyna się jako true, więc pierwsza strona jest rysowana raz przy starcie.
  • W menu pozycja paska pochodzi z numeru strony: 1 + page * 21 daje 1, 22 lub 43, trzy wiersze z Kroku 3.
  • W przeciwnym razie switch wybiera, którą stronę narysować.

Cały szkic obok modelu ekranu pokazującego stronę Heart przy page = 2 i menu = false; linia if (!changed) return jest podświetlona, a pasek mówi wyślij jedną ramkę, 44 ms

W symulatorze wysłanie jednej pełnej ramki na ten wyświetlacz zajmuje około 44 milisekund; zmierzyliśmy sendBuffer() za pomocą micros(). Przerysowywanie przy każdej pętli spędzałoby większość czasu na ponownym wysyłaniu tego samego obrazu. Dzięki changed ekran przerysowuje się tylko po naciśnięciu, a loop() nadal sprawdza przyciski.

drawMenu() pozostawia tryb XOR włączony, więc szkic przełącza się z powrotem na normalne rysowanie za pomocą setDrawColor(1) przed każdą ramką.

Skopiuj każdą kartę do szkicu ​

Kopiuj pobiera kod otwartej karty. Otwórz kartę Clock, kliknij Kopiuj i wklej kod do szkicu, a następnie zrób to samo dla Weather i Heart.

Przed skopiowaniem karty Menu wyłącz opcję Deklaruj obrazy w przełącznikach obok kodu. Menu używa tych samych trzech ikon co strony, a karty stron już zadeklarowały ich tablice. Przełącznik dotyczy całego projektu, więc włącz go z powrotem po zakończeniu.

Po skompilowaniu dla UNO szkic używa 14 588 bajtów flash (45%) i 1619 bajtów RAM (79%). Jak w odcinku 1, większość tego RAM to bufor ramki o rozmiarze 1024 bajtów.

Uruchom to w symulatorze ​

Otwórz projekt symulatora z odcinka 1: Arduino UNO i OLED SSD1306 podłączony do GND, 5V, A5 (SCL) i A4 (SDA), z biblioteką U8g2. Dodaj dwa przyciski i podłącz je:

  • NEXT: jedna nóżka do pinu 2, druga do GND;
  • OK: jedna nóżka do pinu 3, druga do GND.

Wklej szkic z czterema kartami na miejscu i naciśnij Play. Dalej, dalej, dalej: strony krążą i wracają do zegara. OK otwiera menu, dokładnie na bieżącej stronie. NEXT przesuwa kursor na pogodę, a OK ją otwiera. Jedno naciśnięcie, jeden krok.

Symulator uruchamiający szkic: Arduino UNO z OLED i dwoma przyciskami, NEXT na pinie 2 i OK na pinie 3; wyświetlacz pokazuje menu z podświetlonym wierszem Weather

Na prawdziwej płytce podłącz przyciski w ten sam sposób. Popularny przycisk dotykowy ma cztery nóżki połączone parami; jeśli przycisk wydaje się cały czas naciśnięty lub nigdy, użyj dwóch nóżek na przeciwległych rogach.

Podsumowanie ​

  1. Każdy ekran twojego gadżetu jest ekranem w Lopaka: każdy staje się własną funkcją rysowania, a Duplikuj zachowuje układ.
  2. Najpierw narysuj mapę stanów: dwie zmienne, strona i to, czy menu jest otwarte, mogą obsługiwać całe menu.
  3. Wychwyć naciśnięcie, nie przytrzymanie: reaguj na moment, w którym przycisk się wciska, i ignoruj go przez 20 ms po tym.

Następny odcinek: Animacje. Ekrany zaczynają się poruszać: od GIF-a do malutkiego OLED. Przeczytaj odcinek 7 lub obejrzyj go na YouTube.

Gotowy, aby spróbować? Otwórz Lopaka i narysuj ekrany własnego gadżetu.