ESP01 i DHT11 w jednym stały domku

Tak, gdy jakiś czas temu pisałem o module z gniazdem dla ESP01 oraz przekaźnikiem, to oczywistym się wydawało, że do tego powinny powstać podobne moduły o innych, uzupełniających funkcjach. Dość oczywistym jest sensor temperatury i wilgotności. No i taki właśnie pojawił się na Nettigo.

Cóż, widać, że tryb myślenia pana chińczyka jest zbliżony, bo zrobił taki moduł. Choć, z drugiej strony, to tryb myślenia pana chińczyka stanowi pewną zagadkę. Czemu nie ma żadnego otworu montażowego? Czemu nie ma wyprowadzonych pozostałych GPIO? No i czemu ESP01 przykrywa DHT11? To ostatnie to można jeszcze wytłumaczyć chęcią utrzymania niewielkiego rozmiaru, ale jaki to wpływ ma na odczyty temperatury oraz na zasięg WiFi (PCB pod spodem ma GND plane) – tego nie sprawdzałem. Tragedii nie ma, jak jest dobry sygnał to wszystko działa bez zarzutu :)

OK, skończę z tym marudzeniem, bo jeszcze pomyślicie, że to nic niewarty moduł, a tak nie jest. Zwarty (25 mm x 21 mm x 16 mm), wbudowany stabilizator (napięcie zasilające 3.7 – 12 V). Daje radę.

No to teraz, jak z niego skorzystać? Ze sprzętowych spraw – DHT korzysta z jednego pinu do komunikacji i podłączone jest do GPIO02. Teraz – programowanie. Z braku wyprowadzonych dodatkowych pinów, pozostaje programowanie poza układem (albo ładować kod przez sieć, korzystając z OTA, ale i tak pierwszą „pustą” wersję z OTA trzeba wgrać). Dla waszej wygody przygotowałem przykładowy kod, który odczytuje dane i udostępnia przez WiFi w formie JSON.

Do programowania najlepszy jest gotowy konwerter USB/Serial ale jeśli nie macie, to wystarczy z kilku rzeczy zrobić sobie przejściówkę i gotowe. W sumie, może opiszę dokładniej to w oddzielnym wpisie, ale wyglądać może ona tak:

Quasi-programator do ESP01, zrobiony z „beleczego”

W skrócie – użyte zostały złącza 1×8 z przelotkami (odpady w Wemosów, których tutaj pełno się lutuje) obcięte do 1×4, złożone razem i sklejone taśmą (pisałem jakiś czas temu, że to świetny sposób na improwizowane wtyczki żeńskie), płytka obok to rozdzielacz sygnału I2C, ale może służyć do rozmnażania dowolnych sygnałów, napis na PCB nie ma znaczenia… Do tego trochę kabli prototypowych F-F i F-M, tych w szufladzie pewnie masz na pęczki.

Dobra, ale co wgrać?

Dołączam projekt w Plaformio (na końcu wpisu), który realizuje podstawowe zadanie. Przed skompilowaniem kodu plik src/wifi-example.h wyedytuj wpisując właściwe SSID oraz hasło do sieci WiFi. Możesz też zmienić nazwę hosta, domyślnie to będzie dht.local. Po zmianach plik zapisz jako src/wifi.h.

Teraz możesz skompilować kod, wgrać go na ESP01 i po udanym zadaniu przenieść ESP01 do modułu DHT i po podłączeniu zasilania do pinów Vcc oraz GND powinien zacząć działać. Wystarczy wskazać w przeglądarce adres dht.local i (jeśli Twój komputer obsługuje MDNS) to zobaczysz coś podobnego do tego zrzutu ekranu:

Czyli wilgotność względna (tu 49%), temperatura (26.10 °C) oraz ile sekund minęło od pomiaru (tu ok 13.5). W zasadzie tyle Ci wystarczy jeśli chcesz korzystać z tego modułu jako czujki i pobierać dane przez integracje Home Assistant czy Domoticz. Feel free to use.

Opowiedz mi jak to działa…

Jeśli jesteś ciekaw nieco jak działa kod (raczej prosty :) ) to wyjaśnię niżej najważniejsze elementy. Organizacja jest prosta, jeden plik src/main.cpp i plik z danymi logowania do sieci WiFi src/wifi.h. Przykładowy plik z hasłem jest nazwany wifi-example.h i mam go wrzuconego w lokalne repozytorium kodów. Właściwe dane trzeba wrzucić w plik wifi.h, który jest na liście ignorowanych plików .gitignore. W ten sposób dane logowania nie trafią do repozytorium.

Funkcja setup:

Łączymy się z WiFi korzystając z danych w SSID i PASSWORD ustawionych w pliku wifi.h. Kod jest wzięty z przykładów i jeśli już kiedykolwiek używałeś ESP8266 to pewnie dobrze znany.

Kolejny kawałek kodu to inicjalizacja OTA – wgrywania kodu przez sieć. Uwaga – tutaj ostrożnie. Oczywiście to bardzo wygodne rozwiązanie, ale nie zapewnia bezpieczeństwa, więc przemyśl dobrze, czy możesz „wystawić” urządzenie, które pozwala na wgarnie dowolnego kodu. Wystarczy mieć dostęp do tej sieci WiFi.

ArduinoOTA.setHostname(HOSTNAME);
ArduinoOTA.begin(true);

Jeśli chodzi o szczegóły, to najpierw ustawiamy nazwę hosta, która będzie rozgłaszana, a następnie (przez podanie true jako argumentu do begin) wymuszamy by ESP rozgłaszało taką nazwę. Jeśli nie ustawimy nazwy przez setHostname domyślna nazwa to będzie esp8266-XXXXX gdzie XXXX zastąpione zostanie tak zwanym chipID.

Wybiegniemy tutaj trochę, ale same włączenie OTA nie wystarczy. W loop musisz zadbać by regularnie było wywoływane ArduinoOTA.handle(), bez tego nie będzie się dało nic wgrać.

Wracamy do naszej funkcji setup:

server.begin();
server.on("/", root_page);

server to zdefiniowana wcześniej zmienna:

ESP8266WebServer server(80);

To obiekt, który posłuży nam do obsługi serwera WWW. Potrzebujemy jego wstępnej konfiguracji (wystarczy tylko begin()) oraz przypisania funkcji obsługi głównej strony. Tej, która będzie dane w JSON zwracać. Robi to server.on. Jak się może już domyśliliście "/" to ścieżka, która ma być obsługiwana (tutaj jest to po prostu główna strona). root_page to nazwa funkcji która ma być wywołana gdy ktoś „zajrzy” na stronę „/”.

Zwróć uwagę, że tutaj argumentem jest sama nazwa funkcji a nie jej wywołanie (nie ma nawiasów za root_page).

Podobnie jak z OTA – nie wystarczy skonfigurować serwera WWW. Musisz zadbać o to by cyklicznie (w loop) wywoływać server.handleClient() – by serwer WWW mógł działać i odpowiadać na zapytania klientów.

Trzecia sekcja setup zajmuje się konfiguracją DHT11. W tym przykładzie użyłem DHTesp i dopiero teraz pisząc ten tekst zorientowałem się że jest ona nieaktualizowana od pewnego czasu. Działa, ale jeśli chcesz zrobić swój, poważny projekt to może poszukaj innej?

dht.setup(02, DHTesp::DHT11);
delay(dht.getMinimumSamplingPeriod());
getResults();

Tutaj mamy trzy kroki. Wskazujemy GPIO 02 jako pin do komunikacji z DHT, oraz że mamy do czynienia z DHT11. Aha, dht to globalna zmienna stworzona wcześniej.

DHTesp dht;

Drugim krokiem jest odczekanie czasu gwarantującego poprawne odczytanie danych z DHT. Inaczej zamiast wartości liczbowych będzie błąd i pojawi się nan (not an number), którego nie chcesz. Trzeci krok – to wywołanie funkcji getResults, która odczytuje dane z DHT i zapisuje w globalnych zmiennych. Gdyby próbować odczytać dane z DHT bezpośrednio po wywołaniu strony głównej, albo musielibyśmy za każdym razem odczekać ten minimalny czas przed zrobieniem pomiaru albo moglibyśmy dostać nan zamiast rezultatu. Co prawda dla DHT11 ten czas to tylko 1 sekunda, ale, mam jakiś wewnętrzny opór gdy w funkcji obsługującej wywołanie WWW robić delay… Dlatego przyjąłem takie rozwiązanie.

Ponieważ to już wszystko w setup to od razu zobaczmy getResults skoro o niej była mowa:

void getResults() {
temperature = dht.getTemperature();
humidity = dht.getHumidity();
last_measure = millis();
}

temperature i humidity to chyba samo-tłumaczące się nazwy zmiennych (globalnych). Zapisujemy w nich odczytane wartości, a w last_measure zapisujemy wartość millis – bieżącego licznika milisekund od uruchomienia/resetu. Pozwoli to nam cyklicznie odświeżać wartości, bez korzystania z delay. Zasada jest zawsze taka sama, jak ją opanujesz, nie będziesz potrafił pisać program tak, by mógł czekać na upływ czasu nie blokując swojego wykonania.

Więc jak to się robi?

Zajrzyjmy do loop. Poza obsługą OTA i WWW jest tam tylko:

if (millis() - last_measure > 20 * 1000) {
getResults();
}

millis() - last_measure równa się tyle ile milisekund minęło od ostatniego pomiaru. Wystarczy to porównać z zadaną wartością (tutaj 20 sekund). Jeśli jest większa, to można wywołać getResults. Pamiętasz, że pierwszy raz wywołaliśmy getResults w setup? Więc została ustawiona wartość last_measure. Gdyby została wartość 0 (bo tak zdefiniowaliśmy tą zmienną – dobrze jest zawsze nadawać wartości zmiennym przy definicji), bo nie wywołalibyśmy getResults w setup to pół biedy – może się zadanie wykonać nieco wcześniej, tutaj nie miałoby to znaczenia że 20 sekund od resetu a nie końca inicjacji programu (setup może trochę trwać, bo łączenie z WiFi nie zawsze jest błyskawiczne). Dane byłby dostępne wcześniej, nic złego :). Gorzej jeśli w funkcji getResults nie aktualizowalibyśmy wartości last_measure. Co wtedy? Do pierwszego wywołania program czekałby zadany czas (tutaj 20 sekund). Ale gdy millis() - last_measure jest już większe niż zadany interwał, to przy zachowaniu starej wartość last_measure wynik odejmowania będzie dalej rósł. Przy kolejnej iteracji loop warunek byłby dalej prawdziwy i funkcja getResults byłaby wywoływana w każdym przebiegu.

Każdemu to się zdarza – pamiętaj, by zawsze po zrobieniu akcji aktualizować wartość zmiennej przechowującej czas tego wywołania.

Dobra, co zostało? root_page. Jeśli nie pisałeś dotąd programu „robiącego” jakąś stronę WWW (tak, prosty JSON też jest taką stroną) to kilka słów wyjaśnień. Funkcja root_page nie jest nigdzie (wprost) wywoływana w twoim programie. Po przypisaniu jej, jako akcji na wywołanie strony „/” (tak oznacza się stronę główną) to już jest „zmartwienie” klasy ESP8266WebServer by ją wywołać w odpowiednim momencie. Twoim jedynym zadaniem jest wywoływanie cyklicznie funkcji handleClient() (było o tym wcześniej). ESP8266WebServer sam się już zajmie przyjęcie przychodzących zapytań, dopasowaniem właściwej funkcji ścieżki z zapytania. Bo tutaj mamy tylko jedną stronę, ale w innych programach możesz mieć wiele różnych funkcji dopasowanych do różnych ścieżek. Np inną funkcję obsługującą „/” a inną „/config” .

Co jest zadaniem takiej funkcji? Zazwyczaj jest to przygotowanie odpowiedzi (tutaj to będzie JSON z wartościami) i wysłanie do klienta.

JSON nie jest tak prosty jakby się mogło wydawać. By być pewnym, że JSON który generujemy jest poprawny najlepiej byłoby użyć biblioteki ArduinoJSON ale mimo wszystko pokusiłem się o generowanie go „ręcznie”. Jak zawsze – gdy wszystko działa i jest poprawne to nie ma kłopotów. Gorzej gdy pojawiają się jakieś nieprzewidziane sytuacje. Wtedy, generowanie poprawnego JSON może być kłopotliwe. Postanowiłem by zwracany JSON miał wartości tylko gdy są dostępne.

resp = "{";
if (humidity != NAN) {
resp.concat("\"rh\":");
resp.concat(String(humidity));
}

if (temperature != NAN) {
if (resp.length() > 2) {
resp.concat(",");
}
resp.concat("\"temp\":");
resp.concat(String(temperature));
}
if (resp.length() > 2) {
resp.concat(",");
}
resp.concat("\"age\":");
resp.concat(String((millis() - last_measure) / 1000.0));
resp.concat("}");

Stąd ten powtarzający się wzór. if (temperature != NAN) oznacza, że dokleimy wartość temperatury, tylko gdy udało się odczytać z DHT poprawnie wartość. if (resp.length() > 2) to sprawdzenie, czy na którymś wcześniejszym etapie już dopisaliśmy jakąś wartość. Np jeśli nie udało się odczytać z jakiegoś powodu wilgotności a mamy temperature, to nie należy dodawać przecinka, bo będzie JSON miał niepoprawny format ({,"temp":33.0 nie jest poprawne).

Generalnie do łańcucha znakowego resp dopisujemy kolejne elementy. Gdy już skończyliśmy – wysyłamy do klienta. Ja zawsze, z przyzwyczajenia takie rzeczy też wysyłam na Serial – potem przydaje się to do diagnozowania problemów, ale skoncentrujmy się na wysyłaniu do klienta.

server.send(200, "application/json", resp);

Wysyłamy odpowiedź funkcją server.send(). Pierwszy parametr to kod HTTP odpowiedzi. 200 znaczy – wszystko OK, masz moją odpowiedź. W zasadzie, pisząc na ESP8266 to chyba tylko z 200, 301/302 się spotkałem, ale jeśli ktoś ciekaw to pełna lista kodów odpowiedzi HTTP np na Wikipedii.

Drugi argument to tak zwane media type. Informujesz klienta jaki typ danych odsyłasz. Jak widać, JSON to „application/json”. HTML – „text/html”, informacja tekstowa – „text/plain”. Trzeci argument to łańcuch znakowy, który zawiera dane do wysłania do klienta (u nas to będzie JSON, który mozolnie wcześniej przygotowaliśmy).

Co jeszcze?

I to w zasadzie tyle – pisanie aplikacji na ESP8266 zwracające nieco więcej danych, może być czasem związane z wyzwaniami. Dlatego – staraj się zawsze upraszczać komunikację. Jeśli chciałbyś zbudować stronę, która zwiera więcej elementów, to ponieważ ESP8266 ma dość ograniczone zasoby może skutkować powolnym działaniem. ESP8266WebServer jest jednowątkowy i musi skończyć obsługę jednego zapytani nim zajmie się następnym. Strona HTML z wieloma elementami (CSS, JS, obrazki) niby jest do zrobienia na ESP8266, ale ze względu na sekwencyjną obsługę wszystkich wywołań, użytkownik będzie miał wrażenie strasznej powolności. Więc – keep it simple! Najlepiej zwracać małe ilości danych w prostych formatach.

Gdy jednak musisz wysłać coś większego, to możesz to robić w kawałkach. I choć wydaje się że 80 kB RAM to sporo, to łatwo pisząc bardziej rozbudowane programy zacząć mieć z tego tytułu kłopoty – zwłaszcza, jeżeli twoje ESP8266 wysyła rozbudowany HTML do klienta. Wtedy pozostaje skorzystać z funkcji sendContent ale to jest rozwiązanie jeżeli możesz stopniowo generować HTML i go kawałkami wysyłać (kasując to co już wysłane). A może to po prostu moment by zacząć myśleć o innym procesorze lub przemyśleć całą koncepcję działania. Może da się to zrobić prościej?

Do pobrania

Kod do tego wpisu