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.
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 <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
GPIO state: ON
"<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.
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.
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”.
<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.
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.
{
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[].
{
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.
{
// 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.
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