Dziś kontynuujemy przygodę z modułem ESP32 + wyświetlacz dotykowy w wersji ESP32-2432S028R. W tej części poćwiczymy interakcje z wyświetlaczem za pomocą ekranu dotykowego. W tym celu napiszemy tutaj kilka prostych, interaktywnych programów, takich jak mierzenie czasu reakcji użytkownika, quiz matematyczny oraz grę "kółko i krzyżyk".
Na wstępie od razu chciałbym zaznaczyć, że nie jest to jedyny sposób rysowania na tym wyświetlaczu. Wręcz przeciwnie - jest nawet lepszy sposób, możemy na nim uruchomić LVGL i nawet tworzyć interfejsy w odpowiednim do tego programie, ale cierpliwości - LVGL będzie omawiany w kolejnych częściach.
Poprzednie części:
ESP32 i wyświetlacz dotykowy - tutorial - część 1 - jak programować? Podstawy
ESP32 i wyświetlacz dotykowy - część 2 - jak rysować piksele, linie, kształty, kwestia wydajności
Czas reakcji
Na początek wymyśliłem coś bardzo prostego - pomiar czasu reakcji. Chodzi tu o czas reakcji użytkownika, tj. sprawdzamy jak szybko jesteśmy w stanie nacisnąć ekran po tym gdy zmieni on kolor.
Zastanówmy się, jak możemy to zrealizować?
Musimy w pętli:
- ustawiać np. początkowy stan ekranu (powiedzmy czarny ekran)
- czekać przez jakiś czas, dając użytkownikowi czas się przygotować
- nagle zmienić np. kolory ekranu na przeciwne (dać użytkownikowi znak)
- od tego momentu musimy jakoś zmierzyć czas do momentu naciśnięcia ekranu
- potem trzeba ten czas wyświetlić
Do pomiaru czasu użyjemy, tak jak to w Arduino bywa, funkcji millis.
Teraz wpisujmy to kolejno w kod. Etap pierwszy - napis "WAIT" i oczekiwanie (można by je zrobić losowe):
Kod: C / C++
Etap drugi - sygnał do działania i początek pomiaru czasu:
Kod: C / C++
Etap trzeci - pomiar czasu (docelowo pewnie można by zrobić przerwaniem, ale w takim prostym programie pozwoliłem sobie na pętle blokującą wykonanie wątku):
Kod: C / C++
Pętla przerywa się gdy ekran wykryje nacisk. Wtedy pobieramy bieżący czas, odejmujemy od niego zapisany i wyświetlamy wynik:
Kod: C / C++
Prezentacja:
No i pełny kod:
Kod: C / C++
Quiz Matematyczny
W poprzednim programie wykorzystaliśmy podstawowe funkcje rysowania (wyświetlanie tekstu) oraz pobierania wejścia od użytkownika (wykrycie dotyku - tam tylko obecności, true lub false). Teraz pora to nieco rozwinąć. Postawmy sobie dwa zadania:
- chcemy wyświetlać na ekranie nieco więcej (jakieś kształty, może nawet przyciski?)
- chcemy też wykorzystać pozycje nacisku, być może by sprawdzić który z przycisków jest wciśnięty
Docelowo będziemy korzystać raczej z LVGL, tam są gotowe przyciski i inne komponenty interfejsu użytkownika, ale na razie ćwiczymy proste programy dla nauki i satysfakcji.
Dobrym przykładem takiej aplikacji będzie quiz matematyczny.
Działanie będzie obejmować (tak jak wcześniej, w pętli):
- wylosowanie dwóch liczb do operacji (np. mnożenia) i wyświetlenie ich na ekranie
- przygotowanie odpowiedzi A B C D, gdzie jedna z nich jest poprawna a pozostałe odpowiedzi są różne od poprawnej (trzeba będzie losować w pętli - dopóki
- oczekiwanie na wejście od użytkownika (nacisk)
- sprawdzenie który z przycisków został wciśnięty (zrobimy to jakoś prosto, kilka warunków na sztywno)
- sprawdzenie czy została wybrana poprawna odpowiedź
- wyświetlenie odpowiedniego komunikatu w zależności od tego co wybrał użytkownik
Można by też dodać zliczanie ilości poprawnych i złych odpowiedzi.
To zaczynamy, zmienne globalne do zliczania:
Kod: C / C++
Potem całość naszej nowej akcji realizujemy w loop, czyli w pętli. Najpierw czyszczenie ekranu i wyświetlenie bieżącego wyniku.
Kod: C / C++
Trzeba też wylosować jakieś dwie liczby, powiedzmy od 1 do 10, dla wygody.
Kod: C / C++
Potem wylosujmy operację do wykonania na tych liczbach. Będzie urozmaicenie:
Kod: C / C++
Teraz nieco sprawa się komplikuje. Musimy losować pozycje poprawnej odpowiedzi, bo co to by był za quiz, gdzie zawsze poprawną odpowiedzią jest np. A?
Kod: C / C++
Powyższy kod najpierw losuje pozycję poprawnej odpowiedzi a potem losuje koniecznie niepoprawne liczby.
Dalej możęmy już wyświetlić zadanie na ekranie oraz zaznaczyć strefy przycisków:
Kod: C / C++
Tajemnicze "datum" określa sposób centrowania tekstu i zawiera następujące opcje:
TL_DATUM = 0 = Top left
TC_DATUM = 1 = Top centre
TR_DATUM = 2 = Top right
ML_DATUM = 3 = Middle left
MC_DATUM = 4 = Middle centre
MR_DATUM = 5 = Middle right
BL_DATUM = 6 = Bottom left
BC_DATUM = 7 = Bottom centre
BR_DATUM = 8 = Bottom right
MC to skrót od "middle centre", czyli centrujemy tekst.
Najważniejsze jest jednak rysowanie "przycisków". Zakładamy, że przyciski zaczynają się w połowie ekranu, stąd zmienna baseY. Potem w pętli od 0 do 4 (bez 4, czyli tylko 0, 1, 2 i 3), za pomocą reszty z dzielenia (modulo) i dzielenia wybieramy jedną z ćwiartek naszego ekranu. Rysujemy kolejno prostokąty, a potem wyświetlamy w nich tekst.
Potem czekamy na reakcję użytkownika:
Kod: C / C++
Powyższy kod oczekuje na dotyk ekranu a potem oblicza pozycję dotknięcia w pikselach wyświetlacza.
Potem, trochę na piechotę, sprawdzamy która z ćwiartek została "dotknięta". Są tylko cztery, więc wystarczą trzy bloki warunkowe:
Kod: C / C++
Ważne by pamiętać o aktualizowaniu na raz rysowania przycisków oraz ich sprawdzania, bo tutaj zasadniczo ten sam kształt powtarza się dwa razy. Docelowo można by to zrobić lepiej (a nawet wprowadzić klasę przycisku).
Teraz zostaje sprawdzić czy użytkownik wybrał poprawną odpowiedź i wyświetlić odpowiedni komunikat:
Kod: C / C++
Tutaj też jest zliczanie ilości odpowiedzi poprawnych oraz błędnych. Dodatkowo tutaj dodałem sobie wyświetlanie wybranej opcji, to tylko w celu weryfikacji działania programu.
Po tym w kodzie jest tylko jeszcze jeden delay, aby dać użytkownikowi czas na odczytanie komunikatu.
Oto cały kod:
Kod: C / C++
Rezultat:
Kółko i krzyżyk
W poprzednim akapicie już nieco poćwiczyliśmy interakcje z czymś więcej niż tekstem, były cztery interaktywne przyciski, to może teraz zrobić w pełni projekt opierający się na "przyciskach" i kształtach? Jednym z popularniejszych zadań, chociażby na studiach, jest zrobienie gry kółko i krzyżyk. Spróbujmy to tutaj zrealizować. Weźmy tę łatwiejszą wersję, czyli grę dwóch graczy ze sobą. Nie chcę się skupiać tutaj na grze z komputerem.
Ok, co potrzeba do tej gry?
Na pewno:
- rysowanie wizualnie planszy (z linii?)
- rysowanie wizualnie kół i krzyżyków (do koła jest gotowa funkcja, a krzyżyk to tylko dwie linie, nie ma problemów)
- trzeba przechowywać jakoś stan planszy, plansza jest dwuwymiarowa (szachownica), więc może tablica dwuwymiarowa? Tylko jaka jej zawartość - może enumerator (pole), chociaż po co? Może użyjemy znaków ASCII, czyli spacji oraz liter x i o?
- potrzebna jest logika tur i sprawdzania wygranej, ale ona wcale nie jest taka trudna, mamy raptem dwie przekątne, oraz po 3 linie w pionie i poziomie. Te linie można by pętlami robić, więc dwie pętle i dwa warunki..
Więc zacznijmy od kodu. Najpierw stałe, rozmiar planszy (ilość pól) oraz komórki (w pikselach). Potem wspomniana tablica, na początek pusta. Potem zmienna określająca czyj jest ruch.
Kod: C / C++
Teraz rysowanie linii. Tutaj rysujemy pustą planszę, bez znaczków.
Kod: C / C++
Powyższy kod korzysta z pewnej małej optymalizacji, a mianowicie zamiast rysować "zwykłą" linie, korzystamy z funkcji która szybko rysuje linię wedle danej osi, osobno H - horizontal, oraz V - vertical.
Teraz może wspomniane rysowanie znaku krzyżka (x):
Kod: C / C++
Jako argumenty podają tutaj indeksy kolumny i wiersza pola na szachownicy, dopiero w funkcji zamieniam je na piksele.
Teraz analogicznie, rysowanie znaku kółka:
Kod: C / C++
Teraz może pomocnicza funkcja która rysuje symbol z danej pozycji na szachownicy:
Kod: C / C++
Teraz pomocnicza funkcja do stawiania znaczku (zmienia też kolejkę gracza):
Kod: C / C++
Powyższa funkcja też sprawdza czy można postawić znak na danym polu, w przeciwnym razie nic nie jest wykonywane.
Teraz czyszczenie planszy:
Kod: C / C++
Zostało najgorsze -sprawdzenie warunku zwycięstwa.
Trochę tu opcji jest.
Linie poziome, pionowe, oraz dwa skosy.
Zacznijmy od linii poziomych i pionowych - tu pomoże nam pętla.
Kod: C / C++
Powyższy kod można zoptymalizować (na raz sprawdzać obie osie).
To teraz sprawdźmy przekątne:
Kod: C / C++
Powyższy kod czytelnik też może przerobić w jeden blok warunkowy, wystarczy nieco poćwiczyć warunki i operatory logiczne.
Zostało sprawdzić jeszcze jedno - czy jest remis. Kiedy jest remis? Jak nie ma już miejsc na kolejny znaczek. Czyli sprawdzamy, czy w tablicy jest chociaż jeden znak spacji (wedle przyjętego standardu).
Kod: C / C++
Została funkcja setup i loop - ale tam można się domyśleć co dajemy. Gra będzie oczekiwała na wejście od użytkownika i tylko wtedy będzie się aktualizować:
Kod: C / C++
Mapowanie pikseli na nasze komórki też jest proste - po prostu dzielimy na cellSize (operacje wykonujemy tam na liczbach całkowitych).
Rezultat:
Cały kod:
Kod: C / C++
Podsumowanie
Ktoś by mógł zapytać - "czy naprawdę trzeba rysować wszystko na piechotę?". Oczywiście, że nie - ale warto tego spróbować, by mieć jakieś pojęcie o tym jak działają chociażby współrzędne, albo o tym, skąd wiemy która część interfejsu jest naciśnięta.
Mimo to, w dalszych częściach przygody z ESP32-2432S028R będę już od tego odchodzić. Kolejne projekty które zaprezentuję (w tym np. sterowanie oświetleniem przez WiFi bądź odczyty pomiarów z Tasmoty) będę już opierać na LVGL, tak aby nie wynajdywać koła na nowa.
Dodatkowo, w następnej części spróbujemy wykorzystać już łączność WiFi - pokażę jak pobrać jakieś dane z Internetu. Może prognozę pogody poprzez API? Szczegóły wkrótce.
Fajne? Ranking DIY Pomogłem? Kup mi kawę.