Dzisiaj tworzymy kolejny miniprojekt - tym razem będzie to dotykowy kontroler lampy RGB. Sam kontroler będzie bazować na płytce ESP32 + wyświetlacz dotykowy ESP32-2432S028R, natomiast sterować on będzie dowolnym urządzeniem Tasmota/OpenBeken poprzez interfejs HTTP Tasmoty. Komendy będą wysyłane w postaci żądań GET a odpowiedzi będą parsowane z odebranego formatu JSON, który dokumentacja Tasmoty szczegółowo omawia:
https://tasmota.github.io/docs/Commands/
https://tasmota.github.io/docs/Commands/#with-web-requests
Ten temat stanowi kontynuację serii o płytce ESP32-2432S028R:
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
ESP32 i wyświetlacz dotykowy - tutorial cz. 3 - interakcje, gry i zabawy
ESP32 i wyświetlacz dotykowy - tutorial cz. 4 - pogoda z internetu, API, JSON
ESP32 i wyświetlacz dotykowy - część 5 - LVGL w SquareLine Studio
W tym temacie pozwoliłem użyć sobie próbnej wersji SquareLine Studio, ale zasadniczo jest tutaj to zbędne, kontroler RGB można umieścić z poziomu kodu (co zresztą de facto SquareLine Studio robi), kod jego tworzenia też umieszczę.
Krok 1 - sam interfejs UI
Cały projekt jest bardzo atrakcyjny i przyjazny dla początkujących. Potrzebne komponenty mamy gotowe - nawet widget wyboru koloru jest już w LVGL, o czym zresztą można poczytać w ich dokumentacji:
https://docs.lvgl.io/7.11/widgets/cpicker.html
Oprócz tego przyda się slider do kontroli poziomu jasności i może jakiś przycisk on/off.
Dodajemy obiekty w SquareLine Studio:
Tak jak w poprzedniej części, też dodajemy zdarzenia:
Teraz pora zaimplementować zdarzenia, czyli osobno wciśnięcie przycisku, przesunięcie slidera oraz wybór koloru.
Jak na razie bez wysyłania danych do lampy.
Button - zmiana koloru w momencie wciśnięcia (stan lampy jest albo włączony, albo wyłączony):
Kod: C / C++
Wybór koloru - tu trzeba kolor zamienić z upakowanego trybu 16 bitowego na coś nam bliższego, RGB w wygodniejszej postaci:
Kod: C / C++
Rezultat:
[ 7382][I][main.cpp:39] MyColorChange(): Selected color: R=248, G=76, B=0
[ 9884][I][main.cpp:39] MyColorChange(): Selected color: R=128, G=252, B=0
[ 10375][I][main.cpp:39] MyColorChange(): Selected color: R=8, G=252, B=0
No i suwak, który na razie tylko wyświetla w logu zmianę:
Kod: C / C++
Rezultat:
[ 17024][I][main.cpp:19] MySliderChange(): Selected brightness=55
[ 17084][I][main.cpp:19] MySliderChange(): Selected brightness=54
[ 19754][I][main.cpp:19] MySliderChange(): Selected brightness=91
Krok 2 - łączenie wszystkiego w całość
Ten krok jest opcjonalny, gdyż w przypadku Tasmoty możemy wysyłać osobno wartość koloru, poziom jasności oraz stan on/off, ale w przypadku sterowania LEDami bezpośrednio z ESP (np. pasek WS2812) to jednak może się przydać.
A zatem, mamy trzy wartości (kolor, jasność oraz stan on/off) i chcemy je połączyć. Najłatwiej jest je przemnożyć:
Kod: C / C++
Oczywiście tę funkcję trzeba wywoływać po każdej zmianie.
Krok 3 - interfejsu kontroli Tasmoty/OpenBeken
Wróćmy jednak do sterowania urządzeniem Tasmoty. Komunikację zrealizujemy w oparciu o HTTP, to chyba najprostsza opcja, choć w przyszłości można by rozważyć też MQTT. Bez względu na sposób, warto zapoznać się z dokumentacją komend Tasmoty:
https://tasmota.github.io/docs/Commands/
W kwestii HTTP warto przeczytać pokrewny temat, dotyczy on właśnie interfejsu z którego korzystamy:
OpenBeken jako mini hosting HTTP - pisanie stron w Javascript, REST API Tasmota itd
Czyli wysyłać będziemy komendy w formacie:
http://192.168.0.201/cm?cmnd=POWER%20ON
Tak, to samo można łatwo testować w przeglądarce internetowej.
Zacząć raczej trzeba od podłączenia do WiFi:
Kod: C / C++
Teraz trzeba jakoś wysłać komendę. Realizuje to poniższy fragment kodu:
Kod: C / C++
Wysyłanie komendy realizuję w wątku. W przeciwnym razie blokowałbym odświeżanie interfejsu użytkownika na czas oczekiwania na odpowiedź od urządzenia. Mogłoby to trwać to wyjątkowo długo, zwłaszcza jeśli docelowe urządzenie byłoby offline.
Powyższy kod tworzy wątek za pomocą xTaskCreate:
Kod: C / C++
Dokumentacja: https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32/api-reference/system/freertos.html
Adres URL jest przekazywany jako argument do wątku.
Użycie naszej funkcji jest bardzo proste. Wywołujemy tylko (przykładowo):
Kod: C / C++
Teraz trzeba to wykorzystać. Zacznijmy od on/off. Podpinamy je bezpośrednio pod przycisk, bo nasze ręczne liczenie kolorów nie jest już potrzebne:
Kod: C / C++
Analogicznie, suwak:
Kod: C / C++
No i kolor:
Kod: C / C++
Krok 4 - Dwustronna synchronizacja stanu
Jak na razie zrobiliśmy tylko wysyłanie danych do lampy. Brakuje nam natomiast komunikacji w drugą stronę. To znaczy, że jeśli z zewnątrz zmienimy stan naszej lampy, to stan interfejsu dotykowego się nie zmieni. Po prostu ESP nie będzie wiedział, że coś się zmieniło.
W idealnym świecie sama lampa mogłaby wysyłać do nas informacje o zmianach, ale tutaj opieramy się na możliwościach Tasmoty, więc będziemy musieli sami odpytywać lampę o jej stan.
W Tasmocie służy do tego komenda Status.
Wysyłamy:
http://192.168.0.212/cm?cmnd=Status
Przykładowa odpowiedź to:
Kod: JSON
Trzeba to będzie parsować przy użyciu ArduinoJSON, ale podobną rzecz już wykonywaliśmy w części o pobieraniu informacji pogodowych z Internetu.
Gorzej, że musimy zastanowić się co potem z tą odpowiedzią zrobić.
Nie powinniśmy edytować interfejsu użytkownika z innego wątku, a przynajmniej nie w trakcie jego odświeżania.
Trzeba w takim razie w jakiś sposób odebrać odpowiedź z wątku, ale nie jest to takie proste jakby się mogło wydawać. Może użyjemy kolejki thread-safe z FreeRTOS?
https://www.freertos.org/a00018.html
Kolejki z FreeRTOS, jak podobnie inne mechanizmy, dbają o bezpieczeństwo wątków i pozwalają bezpiecznie przekazywać dane między wątkami. Nie możemy tak po prostu współdzielić zasobów między wątkami bez zabezpieczeń, bo może przejść do trudnych w odtworzeniu i naprawieniu błędów.
Definiujemy kolejkę:
Kod: C / C++
Tworzenie kolejki (argumenty to maksymalna ilość i rozmiar elementu):
Kod: C / C++
Teraz trzeba zrobić dodawanie do kolejki, ale moment. Najpierw musiałem też zmienić kod wysyłania GET, tak aby korzystał z HttpClient. Wynika to stąd, że poprzedni kod miał problem z brakiem Content-Length w odpowiedzi na GET:
Kod: C / C++
Powyższy kod zawiera też dodawanie do kolejki - wywołanie xQueueSend. W argumencie określam długi czas oczekiwania na dostępność kolejki oraz mimo to, jeśli dodawanie się nie powiedzie, to zwalniam utworzony bufor. W przeciwnym razie byśmy mieli wyciek pamięci, która szybko by się skończyła..
Odbieranie odpowiedzi - wywoływane z loop:
Kod: C / C++
Pora skompilować i wgrać - tylko po to, by sprawdzić czy kod działa i czy otrzymujemy odpowiedź w głównym wątku.
Connecting to WiFi...
Connected to WiFi
[ 6344][I][esp32-hal-adc.c:235] __analogReadMilliVolts(): ADC1: Characterized using eFuse Vref: 1072
HTTP GET Status = 200
Received 139 bytes: {"Dimmer":59,"Fade":"OFF","Speed":1,"LedTable":"ON","Color":"77,37,0,0,0","HSBColor":"29,100,97","Channel":[30,14,0],"CT":500,"POWER":"ON"}
[ 7253][I][main.cpp:266] checkForReplies(): [MainThread] Received response: {"Dimmer":59,"Fade":"OFF","Speed":1,"LedTable":"ON","Color":"77,37,0,0,0","HSBColor":"29,100,97","Channel":[30,14,0],"CT":500,"POWER":"ON"}Działa! Teraz pora na następny etap, czyli na parsing. Musimy wczytać ten JSON w odpowiednie struktury. Przyda nam się ArduinoJSON:
Kod: C / C++
Podobnie tak jak w przykładzie z pogodą. Deserializujemy JSON:
Kod: C / C++
Potem musimy zastosować pewien trik. Chcemy wspierać zarówno odczyt z pełnego stanu, gdzie trzeba wyszukać element StatusSTS, oraz z okrojonego stanu (gdzie root dokumentu to StatusSTS). W tym celu najpierw szukamy StatusSTS, a jeśli go nie znajdujemy, to zakładamy że sam root jest tym obiektem. To, czy w odpowiedzi otrzymamy od Tasmoty pełny czy okrojony stan zależy od tego jakiej komendy użyjemy.
Kod: C / C++
Jak już wydobędziemy statusSTS, to można wybrać z niego poziom jasności (Dimmer), stan on/off (POWER) i kolor w formacie HSB (HSBColor):
Kod: C / C++
Potem musimy zapisać odebrane informacje do naszych wewnętrznych zmiennych.
Kod: C / C++
Największa zabawa jest z kolorem, bo trzeba go zamienić na RGB. Mamy do tego gotową funkcję z internetu. Warto tu wspomnieć, że ten kolor nie jest przemnożony przez poziom jasności, itd:
Kod: C / C++
Teraz jeszcze potrzeba pomocniczej funkcji SetColorFromBytes, która zasadniczo po prostu ustawia kolor ColorWheel z danego RGB:
Kod: C / C++
Analogicznie, teraz funkcja pomocnicza ustawiająca stan enabled na GUI:
Kod: C / C++
Dla wygody dodałem też argument bSend by określać czy zmiana stanu ma zostać wysłana do docelowego urządzenia czy nie. Jeśli odbieramy stan z tego właśnie urządzenia, to nie ma po co go mu odsyłać.
Sprawdzamy, wszystko działa:
Finalny test na video:
Filmik pokazuje sterowanie lampą z jej natywnego panelu OBK oraz z opracowanego tutaj programu obsługi wyświetlacza dotykowego. Komunikacja jest obustronna, chociaż stany odświeżają się z pewnym małym opóźnieniem. Na filmie widać, że np. jak przesunę pasek poziomu jasności na panelu OBK to przesuwa się on też sam na wyświetlaczu dotykowym.
Podsumowanie
Nasz program działa, lampą da się sterować, a my zasmakowaliśmy podstaw łączenia interfejsu użytkownika z akcjami sieciowymi i znów mieliśmy styczność z formatem JSON. Przy okazji możemy znów docenić LVGL, bo widget wyboru koloru był dla jak znalazł.
Oczywiście jednak to nie koniec przygody, teraz można by jeszcze kod znacznie ulepszyć i zoptymalizować. Zastanawiałbym się np. nad rezygnacją z tworzenia osobno za każdym razem nowego wątku na rzecz pojedynczego wątku "worker thread" który by po prostu realizował operacje sieciowe i zbierał nowe "polecenia sieciowe" z jakiejś kolejki. Ale to miało być tylko krótkie demko więc to sprawa już drugorzędna.
W kolejnej części spróbuję albo ten pokazany tu kontroler zoptymalizować, albo skuszę się już na nieco inny panel, może jakaś forma odczytu z czujników?
Fajne? Ranking DIY Pomogłem? Kup mi kawę.