Środowisko ESP-IDF (4). Serwer WWW

Środowisko ESP-IDF (4). Serwer WWW

Wyposażenie układów ESP32 w sprzętowy interfejs Wi-Fi i dużą ilość pamięci przeznaczonej na program pozwala stworzyć serwer WWW zdolny do współpracy z dowolną przeglądarką internetową. Oznacza to możliwość komunikacji z modułem ESP32 za pomocą urządzenia, takiego jak smartfon czy komputer, bez konieczności pisania specjalistycznego oprogramowania. Wystarczy prosta strona w HTML-u, niekiedy z dodatkiem Java Scriptu, żeby zdalnie sterować modułem.

Działanie serwera WWW

Współpraca serwera WWW z klientem – którym w opisywanej sytuacji jest przeglądarka internetowa – została w sposób uproszczony zobrazowana na rysunku 1. Wymianę komunikatów inicjuje przeglądarka, wysyłając do serwera zapytanie (HTTP Get) o zasób, czyli o stronę internetową. W odpowiedzi serwer zaczyna przesyłać dane żądanej strony (HTTP Response). Po przesłaniu wszystkich składowych witryny – takich jak kod HTML, skrypt Java Scriptu, pliki graficzne, kaskadowe arkusze stylów CSS itp. – połączenie jest kończone. Jeżeli klient chce pobrać kolejną stronę, po kliknięciu np. w link wysyła do serwera kolejne zapytanie. Jeśli serwerem jest moduł ESP32, musi on mieć zapisane w swojej pamięci dane wszystkich obsługiwanych stron. Częścią zapytania – stanowiącego adres URL zasobu – może być również umieszczony w nim dodatkowy kod, np. „/led_on”. Sygnalizuje on modułowi, że pełniąca funkcję interfejsu dioda LED ma zostać załączona; w takim przypadku – oprócz przesłania zawartości strony – moduł włącza zasilanie diody.

Rysunek 1. Współpraca serwera WWW z klientem w postaci przeglądarki internetowej

Budowa oprogramowania serwera WWW na ESP32

W podanym przykładzie sterowana będzie jedna dioda LED podłączona do portu 2 modułu ESP32. Aby oprogramowanie zaczęło działać, należy je przepisać w kolejności wskazywanej przez listingi. Aplikacja napisana została dla ESP-IDF w wersji 4.4, natomiast sam moduł będzie przystosowany do pracy w sieci jako stacja.

Jak pokazano na listingu 1, na początku kodu znajdują się wszystkie niezbędne pliki nagłówkowe. Oprócz tego kod zawiera deklaracje stałych i zmiennych używanych w programie. Do LED_PIN przypisany został numer linii portu IO sterującego diodą LED, w tym przykładzie jest to wyprowadzenie numer 2.

#include <stdio.h> #include <stdlib.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include <esp_http_server.h> #include "esp_wifi.h" #include "esp_event.h" #include "freertos/event_groups.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "driver/gpio.h" #include <lwip/sockets.h> #include <lwip/sys.h> #include <lwip/api.h> #include <lwip/netdb.h> #define LED_PIN 2 static const char *TAG = "espressif"; // TAG for debug int led_state = 0; #define EXAMPLE_ESP_WIFI_SSID "ssid_twojej_sieci"//WIFI SSID #define EXAMPLE_ESP_WIFI_PASS "haslo_twojej_sieci"//PASSWORD #define EXAMPLE_ESP_MAXIMUM_RETRY 5//CONFIG_ESP_MAXIMUM_RETRY /* FreeRTOS event group to signal when we are connected*/ static EventGroupHandle_t s_wifi_event_group; /* The event group allows multiple bits for each event, but we only care about two events: * – we are connected to the AP with an IP * – we failed to connect after the maximum amount of retries */ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static int s_retry_num = 0; Listing 1. Niezbędne pliki nagłówkowe i definicje stałych

W deklaracjach EXAMPLE_ESP_WIFI_SSID oraz EXAMPLE_ESP_WIFI_PASS podajemy nazwę sieci Wi-Fi, w której będziemy pracować, oraz hasło logowania do niej. Z kolei EXAMPLE_ESP_MAXIMUM_RETRY to deklaracja liczby prób nawiązania (lub – w przypadku jego utraty – odzyskania) połączenia z siecią.

Zmienna led_state przechowuje aktualny stan portu IO sterującego LED-em.

Listing 2 zawiera kod HTML generowanej przez serwer strony. Zależnie od stanu portu IO sterującego diodą LED witryna tworzona jest w dwóch wariantach: gdy dioda jest wyłączona – i gdy jest zaświecona. Co do zasady, dane obydwu wariantów są takie same, z wyjątkiem jednej linii

GPIO2 GPIO state: ON
char on_resp[] = "<!DOCTYPE html><html><head> "\ "<title>ESP32 WEB SERVER</title> "\ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> "\ "<style> "\ "html { font-family: Arial; display: inline-block; margin: 0px auto; "\ "text-align: center;} "\ ".card {box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); transition: 0.3s; width: 50%;} "\ ".container {padding: 12px 16px;} "\ ".button { display: inline-block; background-color: #b30000; //red color "\ "border: none; border-radius: 4px; color: white; padding: 16px 40px; "\ "font-size: 30px; margin: 2px;}"\ ".button2 { background-color: #364cf4; //blue color}."\ "</style> "\ "</head> "\ "<body> "\ "<div align=\"center\"> "\ "<h2>ESP32 WEB SERVER</h2> "\ "<div align=\"center\" class=\"card\"> "\ "<p><strong>GPIO2</strong></p><p>GPIO state: <strong> ON</strong></p> "\ "<div class=\"container\"> "\ "<a href=\"/led2on\"><button class=\"container button\">ON</button></a> "\ "<a href=\"/led2off\"><button class=\"button button2\">OFF</button></a> "\ "</div></div></div>" "</body> "\ "</html> "; char off_resp[] = (...) "<p><strong>GPIO2</strong></p><p>GPIO state: <strong> OFF</strong></p> "\ (...) Listing 2. Kod HTML strony generowanej przez serwer

Dla większej przejrzystości kodu miejsce danych, które należy przekopiować z tablicy on_resp[] do off_resp[], oznaczono symbolami (…). Wygląd strony po wyświetleniu w przeglądarce pokazano na rysunku 2.

Rysunek 2. Wygląd strony WWW po wyświetleniu w przeglądarce

Budowa strony jest bardzo prosta i typowa. W sekcji „style” umieszczono definicje wyglądu: użytych czcionek, dwóch przycisków sterujących włączeniem i wyłączeniem LED-a oraz kontenera, w którym klawisze zostały umieszczone. Na listingu 2a można zapoznać się ze szczegółami tej sekcji.

<style> html { font-family: Arial; display: inline-block; margin: 0px auto; text-align: center;} .card { box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); transition: 0.3s; width: 50%; } .container { padding: 12px 16px; } .button { display: inline-block; background-color: #b30000; //red border: none; border-radius: 4px; color: white; padding: 16px 40px; font-size: 30px; margin: 2px;} .button2 { background-color: #364cf4; //blue} </style> Listing 2a. Sekcja kodu strony WWW konfigurująca styl CSS

Natomiast na listingu 2b pokazano kod działającej strony. Najpierw wyświetlane są napisy, w tym informacja o stanie pinu GPIO sterującego diodą LED („GPIO state:”), a następnie kod dwóch klawiszy „ON” i „OFF”. Po naciśnięciu klawisza „ON” z przeglądarki do serwera wysłane zostaje żądanie GET odświeżenia danych strony wraz z komunikatem „/led2on”, natomiast naciśnięcie klawisza „OFF” wysyła komunikat „/led2off”.

<body> <div align="center"> <h2>ESP32 WEB SERVER</h2> <div align="center" class="card"> <p><strong>GPIO2</strong></p> <p>GPIO state: <strong> OFF</strong></p> <div class="container"> <a href="/led2on"><button class="container button">ON</button></a> <a href="/led2off"><button class="button button2">OFF</button></a> </div> </div> </div> </body> Listing 2b. Kod strony WWW

Na listingu 3 pokazano funkcję obsługi zdarzeń. Uwzględnione zostały 3 zdarzenia, rozróżniane przekazywanym w wywołaniu funkcji identyfikatorem.

  • WIFI_EVENT_STA_START jest zdarzeniem inicjacji dostępu do sieci,
  • WIFI_EVENT_STA_DISCONNECTED oznacza zakończoną niepowodzeniem kolejną próbę dostępu do sieci. Po przekroczeniu określonej w EXAMPLE_ESP_MAXIMUM_RETRY liczby prób wysyłany jest na monitor komunikat o błędzie i procedura ponawiania ulega przerwaniu,
  • IP_EVENT_STA_GOT_IP jest zdarzeniem uzyskania dostępu do sieci. Na monitor wysyłany jest wówczas komunikat o numerze IP przydzielonym serwerowi w ramach sieci i serwer przechodzi do nasłuchiwania skierowanych do niego zapytań z przeglądarki.
static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "retry to connect to the AP"); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } ESP_LOGI(TAG, "connect to the AP fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } Listing 3. Procedura obsługi zdarzeń

Listing 4 to rozbudowana procedura connect_wifi(), wywoływana z głównej funkcji app_main() w celu zainicjowania pracy ESP32 w trybie stacji Wi-Fi. Funkcja rozpoczyna się od utworzenia grupy zdarzeń FreeRTOS xEventGroupCreate i zwraca uchwyt do tej grupy. Następnie inicjalizujemy lwIP (za pomocą funkcji esp_netif_init) oraz domyślną pętlę zdarzeń esp_event_loop_create_default dla zdarzeń systemowych, a także konfigurujemy Wi-Fi w trybie stacji z ustawieniami domyślnymi esp_netif_create_default_wifi_sta. Funkcja esp_wifi_init przydziela zasoby do sterownika Wi-Fi i powinna być wywołana przed innymi API Wi-Fi.

void connect_wifi(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); wifi_config_t wifi_config = { .sta = { .ssid = EXAMPLE_ESP_WIFI_SSID, .password = EXAMPLE_ESP_WIFI_PASS, /* Setting a password implies station will connect to all security modes including WEP/WPA. * However these modes are deprecated and not advisable to be used. Incase your Access point * doesn’t support WPA2, these mode can be enabled by commenting below line */ .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "wifi_init_sta finished."); /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) */ EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually happened. */ if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } vEventGroupDelete(s_wifi_event_group); } Listing 4. Procedura connect_wifi()

Następnie – za pomocą dwóch wywołań procedury esp_event_ handler_instance_register – rejestrujemy dwa sterowniki zdarzeń: pierwszy do obsługi wystąpienia jakiegokolwiek zdarzenia Wi-Fi i TCP/IP, drugi – do obsługi zdarzenia uzyskania w sieci adresu IP przez serwer. Funkcja esp_wifi_set_mode ustawia tryb pracy serwera jako stacji, a funkcja esp_wifi_set_config przypisuje SSID i hasło naszej sieci. Końcowa procedura esp_wifi_start uruchamia Wi-Fi zgodnie z wybranymi ustawieniami i przechodzi do zablokowania dalszego wykonywania programu. Następuje oczekiwanie na jedno z dwóch zdarzeń: uzyskanie adresu IP albo na niepowodzenie wielokrotnego (w przykładzie: 5-krotnego) podłączenia do sieci. Proces inicjalizacji Wi-Fi w trybie stacji zostaje zakończony.

Listing 5 zawiera procedury odpowiedzi serwera WWW na zapytania przeglądarki. Funkcja send_web_page wysyła odpowiedź do przeglądarki w postaci wersji strony (wysyłana wersja zależy od aktualnego stanu diody LED, przechowywanego w zmiennej led_state). Przy wyłączonej diodzie funkcja httpd_resp_send wysyła zawartość strony zapisaną w tablicy off_resp[] – natomiast przy włączonej wysyłana jest zawartość tablicy on_resp[].

esp_err_t send_web_page(httpd_req_t *req) { int response; if (led_state == 0) response = httpd_resp_send(req, off_resp, HTTPD_RESP_USE_STRLEN); else response = httpd_resp_send(req, on_resp, HTTPD_RESP_USE_STRLEN); return response; } esp_err_t get_req_handler(httpd_req_t *req) { return send_web_page(req); } esp_err_t led_on_handler(httpd_req_t *req) { gpio_set_level(LED_PIN, 1); led_state = 1; return send_web_page(req); } esp_err_t led_off_handler(httpd_req_t *req) { gpio_set_level(LED_PIN, 0); led_state = 0; return send_web_page(req); } httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = get_req_handler, .user_ctx = NULL}; httpd_uri_t uri_on = { .uri = "/led2on", .method = HTTP_GET, .handler = led_on_handler, .user_ctx = NULL}; httpd_uri_t uri_off = { .uri = "/led2off", .method = HTTP_GET, .handler = led_off_handler, .user_ctx = NULL}; httpd_handle_t setup_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_handle_t server = NULL; if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &uri_on); httpd_register_uri_handler(server, &uri_off); } return server; } Listing 5. Procedury odpowiedzi serwera WWW na zapytania ze strony przeglądarki

Serwer reaguje na zapytania przeglądarki metodą HTTP GET dla trzech URI. Do obsługi „/” przeznaczony jest sterownik get_req_handler, dla „/led2on” sterownik led_on_handler – a dla „/led2off” sterownik led_off_handler. W przypadku pierwszego sterownika odsyłana jest odpowiednia zawartość strony HTML. Dwa ostatnie, przed wysłaniem zawartości strony, także fizycznie modyfikują stan LED-a – odpowiednio: zapalając go lub gasząc.

Funkcja setup_server uruchamia serwer HTTP, zwracając do niego uchwyt.

Listing 6 to główna funkcja „app_main()”. Po procedurach przygotowawczych następuje próba połączenia się z siecią Wi-Fi w trybie stacji. Następnym krokiem jest skonfigurowanie wyprowadzenia GPIO sterującego LED-em. Wstępnie dioda LED zostaje wyłączona, a zmienna led_state – wyzerowana.

void app_main() { // Initialize NVS esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); connect_wifi(); // GPIO initialization gpio_pad_select_gpio(LED_PIN); gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT); led_state = 0; ESP_LOGI(TAG, "LED Control Web Server is running ... ...\n"); setup_server(); } Listing 6. Główna funkcja aplikacji

Wreszcie, po wysłaniu na monitor odpowiedniego komunikatu, serwer zostaje uruchomiony. Na rysunku 3 pokazano przykładowe komunikaty po pomyślnym zalogowaniu w sieci Wi-Fi i przejściu serwera w tryb nasłuchu. Przydzielony adres IP w każdej sieci będzie zapewne inny.

Rysunek 3. Przykładowe komunikaty po pomyślnym zalogowaniu do sieci Wi-Fi i przejściu serwera w tryb nasłuchu

Do opisania procedur użyto materiałów, które można pobrać z [1].

Ograniczenia tej wersji serwera

Przykładowy serwer WWW oparty na protokole HTTP pozwala udostępnić klientom pliki HTML/CSS. Sprawdzi się w najprostszych zastosowaniach, w połączeniach z pojedynczym klientem. Wadą korzystania z biblioteki serwera HTTP jest to, że nie aktualizuje ona automatycznie stanu diody LED na stronie WWW dla wszystkich połączonych klientów. Jeżeli jedno z połączeń włączy lub wyłączy diodę LED, pozostałe tego nie widzą i nie wyświetlają aktualnego stanu. Można rozwiązać opisany problem, używając protokołu komunikacyjnego WebSocket, w którym – po zrealizowaniu przesłania strony – połączenie nie zostanie automatycznie zakończone. Pozwala to serwerowi na odświeżenie stanu strony i przesłanie jej do klienta, np. na skutek zmiany stanu diody LED.

Ryszard Szymaniak, EP

Linki

[1] https://esp32tutorials.com/esp32-web-server-esp-idf/

Artykuł ukazał się w
Elektronika Praktyczna
wrzesień 2024
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów