Kurs Arduino – korzystanie z niedoskonałych bibliotek
W piątym odcinku kursu Arduino nadal zajmujemy się pomiarami ciśnienia, wilgotności i temperatury, ale dodatkowo omówimy korzystanie z dostępnych w Internecie różnych bibliotek i przezwyciężanie trudności, jakie się czasem pojawiają z uwagi na ich niedoskonałości.
Zaawansowane obliczenia
W poprzednim odcinku kursu Arduino (w artykule UR004) cieszyliśmy się, że błyskawicznie udało się zrealizować prostą stację pogodową. Docelowo chcemy jednak wykorzystać Arduino i dostępne czujniki do zdecydowanie poważniejszych celów, mianowicie do monitorowania, a może i regulacji klimatu w mieszkaniu. Dlatego chcemy opanować zaawansowane sposoby wykorzystania czujników temperatury, wilgotności, a także czujników zawartości dwutlenku węgla. Jak pokazuje zamieszczony w tym numerze artykuł z serii „Wokół Arduino”, uzyskana z czujnika wartość wilgotności względnej RH (Relative Humidity) niesie niezbyt dużo informacji, a nawet może wprowadzać w błąd. Często potrzebna jest informacja o wilgotności bezwzględnej (AH – Absolute Humidity) oraz o temperaturze punktu rosy (Dew Point).
Dlatego szukając w Internecie bibliotek do trzyfunkcyjnego czujnik BME280, wybrałem bibliotekę Tylera Glenna, która jak pokazuje to rysunek 1, zrzut ze strony: https://github.com/finitespace/BME280, oferuje takie „obliczenia środowiskowe” (Environment Calculations).
Pakiet Arduino IDE ma wbudowany mechanizm do łatwego, automatycznego pobierania i instalowania bibliotek (Szkic-DołączBibliotekę-ZarządzajBibliotekami). Ja wykorzystałem tę możliwość, instalując najnowszą dostępną aktualnie wersję 2.3.0 tej biblioteki, jak pokazuje rysunek 2.
Napisałem dostępny też tutaj krótki Szkic 1 (A0501).
Szkic 1
#include <BME280I2C.h>
#include <EnvironmentCalculations.h>
#include <Wire.h>
#include <LiquidCrystal.h>
LiquidCrystal lcd(2, 3, 4, 5, 6, 7); // obiekt „lcd”
BME280I2C czujnik; // wartości domyślne obiektu „czujnik”
float tmp, wilg, cis; // tworzymy zmienne globalne typu float
void setup() { //incjalizujemy obiekty:
lcd.begin(16, 2); Wire.begin(); czujnik.begin(); }
void loop() { //w petli co około 1 sekundę odczytujemy:
tmp = czujnik.temp(0);
wilg = czujnik.hum();
// oraz obliczamy temperaturę punktu rosy i wilgotność absolutną:
float dewPoint = EnvironmentCalculations::DewPoint(tmp, wilg, 0); // w °Celsjusza
float aH = EnvironmentCalculations::AbsoluteHumidity (tmp, wilg, 0);
lcd.clear();
lcd.print(„Punkt rosy:”); lcd.print(dewPoint, 1); lcd.write(223); lcd.print(’C’);
lcd.setCursor(0, 1); //w dolnej linii:
lcd.print(„AH:”); lcd.print(aH, 1); lcd.print(„g/m3”);
delay(1000); }
Na początku dołączone są biblioteki, w tym EnvironmentCalculations. Ponieważ program z poprzedniego odcinka kursu działa, znacznie skróciłem szkic. Program odczytuje tylko temperaturę oraz wilgotność do zmiennych tmp i wilg. Następnie wywoływane są funkcje – metody, należące do biblioteki EnvironmentCalculations, służące do obliczania punktu rosy i wilgotności bezwzględnej. Obliczone wyniki wpisywane są do tymczasowych zmiennych dewPoint i aH, a następnie wyświetlane na wyświetlaczu LCD.
Gdy jednak spróbowałem skompilować ten szkic, wykorzystujący pokazaną na rysunku 1 funkcję – metodę float AbsoluteHumidity(), kompilator zgłosił dziwny błąd, jak pokazuje rysunek 3.
Mówiąc najprościej, stwierdził, że funkcja – metoda AbsoluteHumidity()… w ogóle nie należy do biblioteki Environment Calculations! Ale wcześniej przepuścił zawartą w poprzedniej linijce analogiczną funkcję-metodę DewPoint()!
Opis ze strony gihub.com nie zgadza się ze stanem faktycznym!
Korzystając z Arduino i dostępnych bibliotek, należy się spodziewać licznych tego rodzaju przykrych niespodzianek. Między innymi dlatego, że jest to system otwarty, rozwijany przez hobbystów. Niestety, opisy i dokumentacja zwykle nie są najwyższej jakości, a brak znajomości angielskiego to kolejny poważny problem. Część problemów trzeba usuwać metodą prób i błędów, ale dużą zaletą jest fakt, że jest to system otwarty (open source), więc dostępne są wszystkie pliki źródłowe. I trzeba się nauczyć do nich zaglądać. Im wcześniej, tym lepiej.
Aby wyjaśnić zagadkę z rysunku 3, a przy tym wiele się nauczyć, trzeba zajrzeć do plików bibliotecznych. Biblioteki dołączane przez użytkownika znajdują się w katalogu Dokumenty/Arduino, u mnie:
C:\Users\Piotr\Documents\Arduino/libraries
Warto zajrzeć do katalogu z biblioteką BME280 i najpierw do pliku library.properties (rysunek 4).
Po otwarciu tego pliku tekstowego (rysunek 5) za pomocą Notepad++ albo np. Notatnika przekonałem się, że istotnie jest to wersja 2.3.0 biblioteki.
Następnie trzeba wejść niżej do katalogu /src (source), gdzie znajdują się pliki źródłowe – rysunek 6.
Tu są właściwe biblioteki, natomiast w katalogu /examples znajdziemy przykłady.
W naszym szkicu, wykorzystując dyrektywę preprocesora #include, na początku dołączamy dwie biblioteki. A konkretnie wskazujemy dwa pliki pokazane na rysunku 6, o nazwach <BME280I2C.h> oraz <EnvironmentCalculations.h>. Nawias trójkątny < > wskazuje, że preprocesor ma szukać tych plików właśnie w katalogu /Dokumenty/Arduino.
Biblioteki Arduino
Kolejny raz okazuje się, że Arduino IDE to tylko parawan, zasłona, która przykrywa fakt, że korzystamy z języka C (C++). Mianowicie ściślej biorąc, nie ma tu żadnych arduinowych bibliotek!
Biblioteki Arduino okazują się statycznymi bibliotekami języka C++.
Dla najmniej zorientowanych przypomnienie: aby zwiększyć przejrzystość i klarowność programu, można i warto wydzielić standardowe kawałki programu do oddzielnych plików. Pisząc główny program w języku C i C++, wstawimy w nim tylko niezbędne informacje o tych oddzielnych plikach.
Kompilator C++ podczas kompilacji analizuje nasz główny program, kolejno, począwszy od pierwszej linii. Najogólniej biorąc, jeżeli kompilator podczas takiej analizy kolejnych linijek znajdzie cokolwiek, czego nie zna i o czym nie słyszał, to zgłosi błąd. I tak jest na rysunku 3.
Zapewne jednak już wiesz, że na początku procesu kompilacji zgodnie z instrukcjami #incude, preprocesor dołącza biblioteki.
Jak widać na rysunku 6, interesująca nas teraz arduinowa biblioteka obliczeń środowiskowych to tak naprawdę dwa pliki o nazwach: EnvironmentCalculations.h oraz EnvironmentCalculations.cpp.
Ściślej biorąc, właściwe kawałki kodu bibliotecznego zawarte są w plikach .cpp (C++) i zostaną one dołączone dopiero na końcu procesu kompilacji, gdy konsolidator – linker będzie tworzył finalną, kompletną wersję programu, którą zamieni na kod maszynowy procesora.
Natomiast w naszym szkicu 1 mamy polecenie dołączenia jedynie pliku z rozszerzeniem .h. Jest to tak zwany plik nagłówkowy (h od header, nagłówek). Plik nagłówkowy .h nie zawiera „właściwego kodu”, a jedynie informacje wstępne, żeby uspokoić kompilator podczas analizy szkicu. Aby kompilator nie zgłaszał błędów, że czegoś nie zna, w pliku nagłówkowym .h są podane tylko tak zwane prototypy funkcji, inaczej mówiąc deklaracje funkcji. Nie są to właściwe, pełne funkcje, bo jak już wiemy tzw. ciało, definicja czyli finalny kod poszczególnych funkcji danej biblioteki znajduje się w pliku o tej samej nazwie, ale z rozszerzeniem .ccp. W naszym przypadku kompilator ma problem z funkcją-metodą AbsoluteHumidity(), a nie zgłosił błędu w poprzedniej linijce, gdzie jest pokrewna funkcja DewPoint().
Zaglądamy więc do pliku nagłówkowego EnvironmentCalculations.h i znajdujemy tam m.in. prototypy dostępnych funkcji bibliotecznych, w tym prototyp – deklarację funkcji DewPoint(), co jest pokazane na rysunku 7.
Nie ma tu właściwej funkcji, ale jest informacja dla kompilatora, że obliczająca punkt rosy funkcja DewPoint() zwraca obliczoną wartość typu float, a przy jej wywołaniu trzeba jej podać trzy argumenty: wartość temperatury i wilgotności (typu float) oraz wartość logiczną (bool), która określi, czy zwracana wartość będzie „metryczna”. „Metryczna”, „europejska”, a wtedy temperatura punktu rosy będzie wyrażona w stopniach Celsjusza. A „niemetryczna”, „imperialna” da temperaturę w stopniach Fahrenheita.
W pliku nagłówkowym EnvironmentCalculations.h nie znajdziemy natomiast prototypu funkcji AbsoluteHumidity()! Nic więc dziwnego, że kompilator wyrzucił (zgłosił) błąd!
Zainstalowaliśmy automatycznie teoretycznie najnowszą bibliotekę, a oto okazało się, że w Githubie w tym czasie była już dostępna nowsza wersja o większych możliwościach!
Operacje na bibliotekach
Ze strony: https://github.com/finitespace/BME280 ściągnąłem plik plik .ZIP według rysunku 8.
Bez żadnego odinstalowania po prostu skasowałem katalog z biblioteką BME280, a ściągnięty plik ZIP rozpakowałem do katalogu
Dokumenty/Arduino/libraries/
Okazało się, że rozpakowany katalog ma nazwę BME280-master i że pojawił się dodatkowy podkatalog .docs, jak pokazuje rysunek 9.
Po ponownym uruchomieniu szkic 1 (A0501.ino) skompilował się bez problemu.
Rysunek 10 dodatkowo udowadnia, że nowa biblioteka nie wymaga szczególnej instalacji i jest widoczna w Arduino IDE.
Mamy tu dwa bardzo ważne wnioski.
Po pierwsze nie zawsze warto korzystać z wbudowanego w Arduino IDE mechanizmu automatycznego pobierania i aktualizowania bibliotek.
Po drugie okazało się, iż ręczna (de)instalacja bibliotek jest bardzo prosta.
Tylko dla dociekliwych
Gdy znów zajrzymy do pliku nagłówkowego EnvironmentCalculations.h, w nowej wersji oczywiście znajdziemy prototyp funkcji AbsoluteHumidity(), jak pokazuje rysunek 11.
Porównaj go z rysunkiem 7. Także i ta funkcja zwraca wynik typu float, i też przyjmuje trzy argumenty. Zwróć uwagę, że trzeci argument przekazywany do fukcji to jednostka temperatury (TempUnit).
W nowej wersji zmieniono też deklarację funkcji DewPoint(), jak pokazuje rysunek 12.
W starej wersji biblioteki (rysunek 7) za pomocą wartości logicznej (bool) określaliśmy, czy temperatura punktu rosy ma być wyrażona w stopniach Celsjusza czy Fahrenheita, co jest potrzebne w Ameryce.
W nowej wersji korzystamy z parametru TempUnit. Okazuje się, że jest to wewnętrzna, (prywatna) zmienna, ale nie zmienna logiczna typu bool (boolean), tylko niepotrzebnie straszącego początkujących typu enum.
Enum to tak zwany typ wyliczeniowy i jak pokazuje to rysunek 13, w tym wypadku pierwsza wyliczeniowa wartość, czyli liczba 0 będzie oznaczać temperaturę w stopniach Celsjusza, a następna wartość, czyli 1, będzie oznaczać stopnie Fahrenheita.
Możesz się samodzielnie zastanowić, czy podanie jako trzeciego argumentu funkcji DewPoint() wartości 0 oznacza to samo w starej i nowej wersji biblioteki?
Możesz też poeksperymentować ze szkicem 1: w funkcjach DewPoint() i AbsoluteHumidity() jako trzeci argument podaj 1, albo podaj tylko dwa argumenty, bez trzeciego.
Nasuwa się tu trzeci wniosek: biblioteki Arduino przygotowane przez autorów o zróżnicowanym poziomie wiedzy i doświadczenia mogą sprawić rozmaite niespodzianki. Na szczęście przy odrobinie wnikliwości pojawiające się problemy można przezwyciężyć. Zazwyczaj wystarczy zajrzeć do bibliotek, ale trzeba mieć trochę wiedzy na temat języka C (i C++).
A jeśli już weszliśmy tak głęboko, to aż prosi się, żeby bliżej przyjrzeć się bibliotekom. Jednak to szeroki temat i zrobimy to przy innej okazji.
A teraz cieszymy się sukcesem: zbudowaliśmy miernik, który pokazuje punkt rosy i zawartość wody w powietrzu. Mój model zamieszczony jest na fotografii 14.
Jeśli chcesz, możesz rozbudować szkic 1 (A0501), żeby miernik co kilka sekund wyświetlał na przemian temperaturę i wilgotność względną oraz punkt rosy i wilgotność absolutną.
Można byłoby dodać do naszego systemu więcej czujników temperatury, wilgotności i ciśnienia – jeśli chcesz, zrób to samodzielnie.
. Będziemy się też przymierzać do pomiaru zawartości dwutlenku węgla za pomocą czujnika MH-Z19B (fotografia 15).
Piotr Górecki