Lopaka Basics 06: Ekrany i menu

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

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.

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ś:
- Ikona. Otwórz kartę Obrazy, wyszukaj „clock” i kliknij ikonę
clock. Z powrotem w Warstwach ustaw X4i Y3. - Tytuł. Dodaj tekst
Clockczcionkąprofont12na X24, Y14. - Zegar. Dodaj tekst
10:24czcionkąprofont29na X24, Y46. - Kropki. Wybierz narzędzie elipsa (ikona koła) i narysuj trzy małe koła z RX i RY
2, wszystkie na Y56: pierwsze na X54z włączoną opcją Wypełniony, następnie na X62i X70z 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.

Oto kod ekranu 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]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ę
clocki naciśnij Delete. W Obrazach wyszukaj „sun”, dodajweather_suni ustaw X4, Y3. - Teksty. Zaznacz
string 2i wpiszWeather. Zaznaczstring 3, wpisz23Ci ustaw X na40: trzy znaki po 16 pikseli dają 48, a (128 − 48) / 2 = 40. - Kropki. Otwórz grupę
dots, wyłącz Wypełniony dlaellipse 4i włącz dlaellipse 5.

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ą:
| Wiersz | Ikona na X 6 | Etykieta czcionką profont12 na X 26 |
|---|---|---|
| Clock | clock, Y 3 | Clock, Y 15 |
| Weather | weather_sun, Y 24 | Weather, Y 36 |
| Heart | heart, Y 45 | Heart, 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.
- Wybierz narzędzie prostokąt, przeciągnij prostokąt i ustaw X
2, Y1, W124, H21. - 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.

Pozycja Y paska jest teraz zmienną, nazwaną od warstwy:
// [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.

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.

Wychwyć naciśnięcie, nie przytrzymanie
Oczywisty kod mówi: jeśli przycisk jest wciśnięty, przejdź do następnej strony.
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.

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.

To właśnie robi funkcja pressed() w szkicu:
// 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:
#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.
% 3to 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.
changedzaczyna się jakotrue, więc pierwsza strona jest rysowana raz przy starcie. - W menu pozycja paska pochodzi z numeru strony:
1 + page * 21daje 1, 22 lub 43, trzy wiersze z Kroku 3. - W przeciwnym razie
switchwybiera, którą stronę narysować.

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.

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
- Każdy ekran twojego gadżetu jest ekranem w Lopaka: każdy staje się własną funkcją rysowania, a Duplikuj zachowuje układ.
- Najpierw narysuj mapę stanów: dwie zmienne, strona i to, czy menu jest otwarte, mogą obsługiwać całe menu.
- 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.