OpenWeatherMap to usługa która oferuje m. in. dostęp do bieżącej informacji pogodowej dla danej pozycji na mapie. Dane te można łatwo pobrać za pomocą jednego zapytania GET, choć wcześniej potrzebny będzie nam klucz API, który na szczęście można otrzymać za darmo. Tu pokażę jak właśnie takie zapytanie GET wysłać, tym razem jednak bez zewnętrznych bibliotek.
Ten temat opiera się zasadniczo na mojej wcześniejszej prezentacji OWM:
ESP32 i wyświetlacz dotykowy - tutorial cz. 4 - pogoda z internetu, API, JSON
Będzie tu jednak zasadnicza różnica, a mianowicie nie użyję tutaj bibliotek Arduino, całość spróbuję uruchomić w czystym C na najzwyklejszych socketach, które powinny być dostępne na większości platform - w tym też na Windowsie (Winsock). Również parsing JSON zrealizuję w najprostszy możliwy sposób, bez zewnętrznych bibliotek.
Motywacja tematu jest prosta - chciałem uruchomić sobie pobieranie pogody na mikrokontrolerze w sposób wydajny i oszczędny, jak najbardziej zmniejszając użycie pamięci Flash. Docelowo chcę uruchomić kod na socketach z LWIP, czyli na rozwiązaniu dostępnym na wielu systemach wbudowanych.
Uruchomienie na Windowsie
A więc zacznijmy. Pierwszą, dość pozytywną dla nas obserwacją jest fakt, że sockety są też na Windowsie, więc prototyp zrobimy normalnie na komputerze...
Przypomnijmy sobie poprzedni kod:
Kod: C / C++
Zasadniczo musimy zaimplementować tylko ten fragment:
Kod: C / C++
I tu jest pierwsza niespodzianka - za http.GET kryje się nie jedno żądanie, lecz dwa:
- najpierw musimy zamienić nazwę domenową api.openweathermap.org na adres IP (o ile nie mamy tego w cache)
- potem dopiero na ten adres IP musimy wysłać zapytanie GET, czyli zasadniczo krótki pakiet HTTP poprzez protokół TCP
Do pobrania IP dla nazwy domenowej służy funkcja gethostbyname, która dostępna jest zarówno na Windowsie/Linuxie, jak i np. w popularnej bibliotece sieciowej LWIP.
Uruchomienie socketów na Windowsie wymaga jeszcze wywołania WSAStartup, co też uwzględniłem w kodzie:
Kod: C / C++
Rezultat:
Mamy już IP, teraz pora wysłać żądanie GET...
Najpierw otwieramy socket TCP (SOCK_STREAM) i nawiązujemy połączenie:
Kod: C / C++
Powyższy warunek będzie skutkować błędem jeśli na docelowym adresie IP nie ma nasłuchu TCP na określonym porcie. Po pomyślnym nawiązaniu połączenia możemy wysłać żądanie GET, czyli kolejno nagłówek GET zgodny z HTTP 1.1 wraz z adresem interesującego nas zasobu a potem pozwolenie na zamknięcie połączenia po dokonaniu transakcji:
Kod: C / C++
Docelowo adres zasobu złożymy za pomocą sprintf (albo lepiej - snprintf, respektując rozmiar bufora), ale na razie na próbę dałem sztywno.
Potem zostało tylko odebrać odpowiedź i wyświetlić ją na ekranie:
Kod: C / C++
Oczywiście nie zapominajmy o zamknięciu socketa:
Kod: C / C++
Rezultat:
Teraz pora na przetwarzanie odpowiedzi. W przypadku metody opartej o gotową bibliotekę dostawaliśmy od razu sam plik JSON, ale tu odpowiedź zawiera też nagłówek HTTP. Musimy go pominąć. Sprawa jest prosta - dwa przejścia do następnej linii pod rząd oznaczają jego koniec:
Kod: C / C++
Zrzut ekranu, tym razem z debuggera Visual Studio:
Teraz pora wyciągnąć dane z tego ciągu znaków JSON...
Rozważmy jego budowę (bez sztucznego formatowania):
{"coord":{"lon":-74.0059,"lat":40.7127},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"base":"stations","main":{"temp":274.3,"feels_like":269.25,"temp_min":272.46,"temp_max":275.13,"pressure":1024,"humidity":71,"sea_level":1024,"grnd_level":1022},"visibility":10000,"wind":{"speed":5.81,"deg":311,"gust":5.81},"clouds":{"all":100},"dt":1733227560,"sys":{"type":1,"id":4610,"country":"US","sunrise":1733227402,"sunset":1733261345},"timezone":-18000,"id":5128581,"name":"New York","cod":200}
Całość wygląda bardzo prosto, klucz od temperatury ("temp") jest wzięty w cudzysłów, a po nim jest dwukropek i jego wartość. Możemy go wyszukać w łańcuchu znaków za pomocą strstr, ale wcześniej dopiszemy do niego cudzysłów i ten dwukropek. Rezultat można zamienić z napisu na liczbę zmiennoprzecinkową za pomocą atof:
Kod: C / C++
Przykład użycia funkcji:
Kod: C / C++
Rezultat:
Działa! Tylko czemu ta wartość temperatury jest taka dziwna?
Poprawka jednostki temperatury
OpenWeatherMap domyślnie wysyła temperaturę w Kelwinach. Zgodnie z dokumentacją:
Cytat:
Temperature is available in Fahrenheit, Celsius and Kelvin units. Kelvin is used by default, with no need to use the units parameter in API calls.
For temperature in Fahrenheit, use "units=imperial".
For temperature in Celsius, use "units=metric".
You can find examples of API calls in the documentation for the service you are interested in.
aby otrzymać wyniki w °C, należy do zapytania dopisać wybór jednostki:
Nieco lepiej, czy naprawdę w Nowym Jorku jest teraz jeden stopień?
No, prawie. Powiedzmy, że się zgadza.
Uruchomienie na mikrokontrolerze (tutaj dla przykładu BK7231)
W przypadku BK7231 musiałem przede wszystkim załączyć inne nagłówki, tu sockety udostępnia wspomniany LWIP:
Kod: C / C++
Dodatkowo, całość musiałem umieścić w wątku, bo operacje send, recv, a nawet i connect, są tutaj blokujące:
Kod: C / C++
Rezultat (na mojej platformie OBK):
JSON jest poprawnie odbierany, teraz zostało go przetworzyć wedle uznania.
Podsumowanie
Jak widać wcale nie potrzeba zewnętrznych bibliotek by móc szybko i sprawnie uruchomić pobieranie bieżącej sytuacji pogodowej z API OpenWeatherMap. Co więcej, całość da się często uruchomić normalnie na komputerze z systemem Windows (czy tam Linux - tam też są sockety zgodne z moją prezentacją) a potem wygodnie przenieść na używaną przez nas platformę wbudowaną... w moim przypadku docelową platformą było BK7231 z LWIP, ale myślę, że nie tylko BK ten sposób może dotyczyć.
Również samo wyłuskanie pomiarów z JSON okazało się być proste. Są do tego gotowe, zaawansowane biblioteki takie jak cJSON, ale jak widać, nie zawsze są koniecznie potrzebne...
Czy widzicie jakieś zastosowanie dla tego API? Może jakiś wyświetlacz?
PS: Oczywiście przedstawiony kod jest uproszczony, można by go ulepszyć, np. zabezpieczyć przed przekroczeniem rozmiaru buforu w funkcji szukającej w JSON, czy też można by go zoptymalizować, np. tworząc szukany łańcuch ręcznie, bo użyty w tym momencie tam sprintf jest dość ciężki...
Fajne? Ranking DIY Pomogłem? Kup mi kawę.