Ta część kursu rozpoczyna rozdział związany z modułem Wi-Fi, oraz szeroko rozumianym networking’iem. Postaram się w jak najbardziej przystępny sposób przedstawić te co by nie mówić, dość skomplikowane zagadnienia. Celem będzie przebrnięcie od najbardziej podstawowych kwestii, jak omawiane w tej części łączenie się z siecią WiFi, przełączanie esp w tryb Access Point; do bardziej skomplikowanych rzeczy jak stawianie serwera http, pobieranie danych z sieci, czy uruchamianie na esp popularnych protokołów sieciowych.
Wszelkie kody znajdziesz na githubiie – https://github.com/mcinm/esp-idf/tree/main/kurs12https://github.com/mcinm/esp-idf/tree/main/kurs12
WiFi | ESP-IDF
Inicjalizacja WiFi
W przypadku esp-idf Wi-Fi/Networking obsługiwane jest za pomocą trzech bibliotek/API:
- esp_wifi – Obsługujemy za jej pomocą sprzętowy moduł Wi-Fi
- lwip (lightweight TCP/IP stack) – To otwarto-źródłowa biblioteka obsługująca stos TCP/IP i protokoły sieciowe dla układów wbudowanych, z której korzysta esp-idf
- esp_netif (ESP Network Interface) – API do komunikacji ze stosem TCP/IP
#include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_log.h" #include "nvs_flash.h" void wifi_init(void) { nvs_flash_init(); ESP_ERROR_CHECK(esp_netif_init()); esp_netif_create_default_wifi_sta(); wifi_init_congif_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_start()); }
Powyżej przedstawiłem prostą funkcje, której zadaniem jest inicjalizacja modułu wifi jako stacja (klient).
Na samym początku wywołuje funkcję „nvs_flash_init()”. NVS (Non Volatile Storage) to biblioteka umożliwiająca przechowywanie danych w pamięci flash pod postacią słów kluczowych. Musimy ją zainicjować, gdyż tego wymaga moduł wi-fi.
Polecenia zawierające w sobie „esp_netif” służą do operowania na stosie TCP/IP. ESP-NETIF (Network Interface) to API dostarczane przez producenta, dzięki któremu nie musimy fizycznie działać na stosie, a mamy do tego proste polecenia. Pierwsze polecenie inicjalizuje stos TCP/IP, a drugie tworzy obiekt esp-netif z domyślnymi ustawieniami dla stacji (klienta).
Następne trzy wiersze zawierają funkcję konfigurujące sprzętowy moduł Wi-Fi z domyślnymi ustawieniami. A ostatni włącza Wi-Fi.
Skanowanie pobliskich sieci
Ten przykład działa także dla środowiska esp8266, wystarczy zamienić funkcję inicjalizacji wifi.
Pierwszym naszym przykładem faktycznego użycia podzespołu Wi-Fi będzie przeskanowanie znajdujących się wokół nas sieci bezprzewodowych. W tym celu skorzystam z przykładu producenta, trochę go jednak upraszczając.
#include <string.h> #define DEFAULT_SCAN_LIST_SIZE 10 static const char *TAG = "scan"; static void print_auth_mode(int authmode) { switch (authmode) { case WIFI_AUTH_OPEN: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OPEN"); break; case WIFI_AUTH_OWE: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OWE"); break; case WIFI_AUTH_WEP: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WEP"); break; case WIFI_AUTH_WPA_PSK: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_PSK"); break; case WIFI_AUTH_WPA2_PSK: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_PSK"); break; case WIFI_AUTH_WPA_WPA2_PSK: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_WPA2_PSK"); break; case WIFI_AUTH_WPA2_ENTERPRISE: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_ENTERPRISE"); break; case WIFI_AUTH_WPA3_PSK: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA3_PSK"); break; case WIFI_AUTH_WPA2_WPA3_PSK: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_PSK"); break; default: ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_UNKNOWN"); break; } }
Do naszego kodu inicjalizującego Wi-Fi musimy dołączyć bibliotekę „string.h”, ta będzie nam potrzebna do wykonania jednaj funkcji. Dodatkowo definiuje tutaj makro, które określa ile sieci maksymalnie będziemy chcieli zapisać, a także wkleiłem tutaj funkcje (z przykładu), która ustala typ zabezpieczenia sieci bezprzewodowej.
Teraz funkcja skanująca:
void wifi_scan(void) { uint16_t number = DEFAULT_SCAN_LIST_SIZE; wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE]; uint16_t ap_count = 0; // Ustawia zera na wszystkie elementy ap_info memset(ap_info, 0, sizeof(ap_info)); // Rozpoczęcie skanowania esp_wifi_scan_start(NULL, true); /* Zapisuje wszelkie informacje o pobliskich Access Point'ach w wcześniej utworzonej przez nas strukturze. */ ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info)); // Zapisuje w podanej przez nas zmiennej liczbę znalezionych Access Point'ów ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count)); ESP_LOGI(TAG, "Liczba zeskanowanych AP's: %u", ap_count); for (uint8_t i=0; i<ap_count); i++) { ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid); ESP_LOGI(TAG, "RSSi \t\t%d", ap_info[i].rssi); print_auth_mode(ap_info[i].authmode); ESP_LOGI(TAG, "Channel \t\t%d\n", ap_info[i].primary); } }
Chyba wystarczająco klarownie opisałem co się tutaj dzieje za pomocą komentarzy. Pominąłem sprawdzanie sposobu szyfrowania, tak jak pierwotnie było to w przykładzie espressif. Zostawiłem tylko te podstawowe informacje. W dokumentacji znajdziesz wszystkie zmienne jaki przechowuje struktura „wifi_ap_record_t”:
Odniosę się tylko do funkcji „esp_wifi_scan_get_ap_records(&number, ap_info)”.
Zmienną „number” przekazujemy tutaj przez pointer, mimo że mamy ją sztywno zdefiniowaną wartością 10. Robimy tak, dlatego że gdy ją przekazujemy stanowi ona maksymalną ilość sieci jakie funkcja zapiszę w pamięci, ale jeżeli jest ich mniej to podmienia wartość zmiennej „number”, na ich liczbę. Jeżeli esp zeskanuje 5 sieci wokół, to „number” przyjmie taką wartość.
Jedyne co pozostało to wykonanie obu funkcji w pętli głównej, w celu sprawdzenia działania programu.
void app_main(void) { wifi_init(); wifi_scan(); }
Efekt na podglądzie portu szeregowego powinien wyglądać w ten sposób.
ESP32 WiFi Access Point
ESP32 może działać także jako Access Point, czyli rozgłaszać własną sieć WiFi. Raczej rzadziej się to wykorzystuje, ale czasami jest bardzo przydatne. Na przykład w sytuacji gdy budujemy urządzenie, z którym od czasu do czasu musimy się połączyć by je przeprogramować, ale nie jest wymagana ciągła jego dostępność w sieci.
Przykład ten pokazuje tylko proste postawienie sieci, jej obsługą i konfiguracją zajmiemy się później.
W zasadzie wszystko co musimy zrobić to nieco zmodyfikować funkcje inicjalizacji WiFi.
#define SSID "ESP32" #define PASSWD "12ASD345" #define MAX_CLIENTS 4 #define CHANNEL 0 void wifi_init(void) { nvs_flash_init(); ESP_ERROR_CHECK(esp_netif_init()); esp_netif_create_default_wifi_ap(); wifi_init_congif_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); wifi_config_t conf = { .ap = { .ssid = SSID, .password = PASSWD, .max_connection = MAX_CLIENTS, .channel = CHANNEL, .authmode = WIFI_AUTH_WPA_WPA2_PSK, .ssid_hidden = 0, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &conf)); ESP_ERROR_CHECK(esp_wifi_start()); }
Po załadowaniu tego programu, esp zacznie rozgłaszać sieć z SSID „ESP32”. Możemy bez problemu się z nią połączyć.
W podglądzie portu szeregowego widać jaki adres IP został przypisany nowemu urządzeniu.
ESP32 WiFi Client
Najważniejsza kwestia, czyli uruchomienie modułu Wi-Fi w trybie stacji (klienta). Celem programu będzie proste połączenie się z naszą siecią Wi-Fi. Warto tutaj pamiętać, że esp nie ma sztywno przypisanego adresu MAC, podobnie jak nazwy hosta – w pełni możemy te parametry modyfikować, co z resztą tutaj zaprezentuje. Moduły esp połączysz tylko z sieciami działającymi na 2.4 GHz.
Zmodyfikujmy więc funkcje odpowiedzialną za inicjalizacje Wi-Fi.
void wifi_init(void) { nvs_flash_init(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_t *netif_handle = esp_netif_create_default_wifi_sta(); esp_netif_set_hostname(netif_handle, "MCINM"); wifi_init_congif_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); wifi_config_t conf = { .sta = { .ssid = SSID, .password = PASSWD, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); uint8_t mac[6] = {0x70, 0xB3, 0xD5, 0x6A, 0x02, 0x01}; esp_wifi_set_mac(WIFI_IF_STA, mac); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &conf)); ESP_ERROR_CHECK(esp_wifi_start()); }
Zauważ, że wykonanie funkcji inicjalizującej nasz interfejs sieciowy przypisałem teraz do zmiennej odpowiedniego typu. Zmienna ta będzie teraz uchwytem, za pomocą którego będziemy mogli dokonywać zmian w konfiguracji interfejsu sieciowego, lub pobierać informacje dla nas istotne.
Z pomocą tego uchwytu zmieniłem nazwę hosta na „MCINM”.
Dalej nie ma już nic szczególnego, poza zdefiniowaniem naszego adresu mac. Zwróć uwagę, że to robimy na warstwie biblioteki esp_wifi.
Na poniższych obrazkach pokazane jak to wygląda w terminalu i podglądzie urządzeń podłączonych do mojego routera.
By pokazać, że połączenie działa, już tutaj podam bardzo ciekawe zastosowanie dla tych mikroklocków. Wykonam zapytanie GET do dowolnego serwera http.
By to zrobić, zaimportuje bibliotekę odpowiedzialną za obsługę protokołu http i napiszę dwie dodatkowe funkcje.
#include "esp_http_client.h" esp_err_t event_get(esp_http_client_event_handle_t get) { switch (get->event_id) { case HTTP_EVENT_ON_DATA: printf("%.*s\n", get->data_len, (char*)get->data); break; default: break; } return ESP_OK; } void get_req(void) { esp_http_client_config_t config = { .url = "http://piteusz.ovh", .method = HTTP_METHOD_GET, .cert_pem = NULL, .event_handler = event_get }; esp_http_client_handle_t req = esp_http_client__init(&config); esp_http_client_perform(req); esp_http_client_cleanup(req); } void app_main(void) { wifi_init((); vTaskDelay( 5000 / portTICK_PERIOD_MS); get_req(); }
Nie zamierzam teraz wyjaśniać co się tutaj dokładnie dzieje. Po wykonaniu programu, w podglądzie portu szeregowego powinniśmy ujrzeć kod strony Piteusza.
WiFi | ESP8266 RTOS SDK
Inicjalizacja WiFi
Tutaj inicjalizacja wygląda bardzo podobnie do przykładu z ESP-IDF. Nie ma sensu kolejny raz tego szczegółowo opisywać. Zauważ jednak, że funkcje biblioteki „esp_netif” tutaj wyglądają nieco inaczej. „tcpip_adapter_init()” to właśnie polecenie pochodzące z tej biblioteki, początkowo na ESP-IDF też to tak wyglądało, ale biblioteka tam została przebudowana.
Tutaj podobnie jak już w przykładach z ESP-IDF dorzuciłem funkcję „esp_event_loop_create_defualt”. Co prawda nie jest ona nam w tych przykładach potrzebna, jednak robiąc w przyszłości projekty na esp, „pętli zdarzeń” używać będziesz praktycznie zawsze. Uznajmy więc, że to część inicjalizacji.
#include "freertos/FreeRTIS.h" #include "freertos/task.h" #include "esp_system.h" #include "driver/uart.h" #include "esp_netif.h" #include "esp_wifi.h" #include "nvs.h" #include "nvs_flash.h" void wifi_init(void) { nvs_flash_init(); tcpip_adapter_init(); esp_event_loop_create_default(); wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&config); wifi_config_t net_conf = { .sta = { .ssid = SSID, .password = PASSWD }, }; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(ESP_IF_WIFI_STA, &net_conf); esp_wifi_start(); esp_wifi_connect(); }
ESP8266 WiFi Client
Poniższy kod przedstawia bardzo prostą procedurę połączenia esp8266 z siecią Wi-Fi.
#include "freertos/FreeRTIS.h" #include "freertos/task.h" #include "esp_system.h" #include "driver/uart.h" #include "esp_netif.h" #include "esp_wifi.h" #include "nvs.h" #include "nvs_flash.h" #define SSID "ssid_sieci" #define PASSWD "hasło" void wifi_init(void) { nvs_flash_init(); tcpip_adapter_init(); esp_event_loop_create_default(); wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&config); wifi_config_t net_conf = { .sta = { .ssid = SSID, .password = PASSWD }, }; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(ESP_IF_WIFI_STA, &net_conf); esp_wifi_start(); esp_wifi_connect(); } void app_main() { wifi_init(); }
Nie będę pokazywał programów dla ustawienia esp8266 w tryb Access Point, czy skanowania Wi-Fi bo wygląda to identycznie jak w przypadku ESP-IDF.
Event Handling WiFi
W środowiskach espressif zaimplementowana jest funkcja event’ów. Eventy to sygnały powodowane przez niektóre komponenty mikrokontrolera. Dzięki nim, możemy natychmiastowo reagować na różne sytuacje.
Takie sygnały wywołuje między innymi omawiany tutaj, moduł Wi-Fi i dzięki temu moduł nas poinformuje gdy np. połączy się z siecią, otrzyma adres IP, czy straci połączenie.
Posłużę się tutaj kodem z przykładu środowiska ESP-IDF.
#define EXAMPLE_ESP_MAXIMUM_RETRY 10 static int s_retry_num = 0; static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { /* Program sprawdza, czy ma do czynienia z eventem wywoływanym przez moduł wifi i czy jest to sygnał wystartowania modułu wifi, jeżeli tak łączy z siecią */ if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } // Ta procedura sprawdza, czy ma do czynienia z eventem wywoływanym przez moduł wifi // i czy jest to sygnał rozłączenia z siecią wifi, // jeżeli tak to stara się z nią połączyć zdefiniowaną wcześniej ilością prób 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"); } // Jeżeli to mu się nie uda zwraca niepowodzenie else { } ESP_LOGI(TAG,"connect to the AP fail"); } // Tutaj program sprawdza, czy ma do czynienia z eventem wywoływanym przez stack IP // i czy jest to sygnał informujący o otrzymaniu adresu IP // jeżli tak to przypisuje przekazywane przez sygnał dane do struktury odpowiedniego typu // i ładnie nam reprezentuje adres IP na ekranie 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; } }
Dla esp8266 zamienimy tylko ostatniego if’a:
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%s", ip4addr_ntoa(&event->ip_info.ip)); retry_sum = 0; }
To jest kod obsługujący nasze wydarzenia.
Deklarujemy funkjcę, która przyjmuje kolejno:
- event_base – To jest informacja jakiego komponentu dotyczy wydarzenie
- event_id – Tutaj już przekazywane jest id konkretnego wydarzenia, przykładowo „WIFI_EVENT_STA_START”
- event_data – Dane jakie przekazuje funkcji, sygnał, może tam się znaleźć chociażby adres IP
My podepniemy tylko dwa rodzaje sygnałów. Odpowiedzialne za moduł Wi-Fi, oraz stack IP. A więc sprawdzamy tutaj tylko dwie podstawy eventów; WIFI_EVENT i IP_EVENT.
ID eventów związanych z Wi-Fi oraz IP możesz znaleźć pod tym linkiem: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/event-handling.html
Teraz podepniemy tę funkcję jako obsługująca eventy:
-------------ESP32------------- esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id); esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip); -------------ESP8266------------- esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL); esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL);
Dodaj komentarz