W tym poście chciałbym przedstawić w jaki sposób uruchomić ESP32, tak aby działało jak punkt dostępu. Dzięki temu można się do niego jak do wifi, po czym odpalić na nim np. własną stronę internetową. Co przedstawię w poniższym przykładzie.
Konfiguracja ESP jako punktu dostępu:
Dla konfiguracji ESP32 jako punktu dostępowego do dyspozycji jest następująca struktura:
Jej parametry to:
- ssid - czyli Service Set Identifier. Jest to identyfikator sieci, który może składać się z maksymalnie 32 znaków.
- password - hasło jakie musi być podane aby uzyskać dostęp do sieci.
- ssid_len - długość SSID, może być ustawione na zero. Wtedy nastąpi wyszukiwanie końca znaku w ciągu
- channel - jest to numer kanału transmitującego
- authmode - poziom zabezpieczenia sieci:
- WIFI_AUTH_OPEN - otwarta, nie potrzeba podawać hasła;
- WIFI_AUTH_WEP - zabezpieczony dostęp do sieci;
- WIFI_AUTH_WPA_PSK - zabezpieczony dostęp z szyfrowaniem TKIP lub AES;
- WIFI_AUTH_WPA2_PSK - zabezpieczony dostęp z szyfrowaniem AES;
- WIFI_AUTH_WPA_WPA2_PSK - wykorzystuje wpa lub wpa2;
- WIFI_AUTH_WPA2_ENTERPRISE - do jego wprowadzenia potrzeba serwera RADIUS. Przydziela on klucze odpowiednim użytkownikom.
- ssid_hidden - pozwala na ukrycie sieci bez przesyłania SSID.
- max_connection - maksymalna liczba jednoczesnych połączeń. Domyślnie 4, maksymalnie też 4.
- beacon_interval - jest to interwał czasowy dla ramki nawigacyjnej. Jest ona wysyłana przez punkt dostępowy zwykle co 100ms. Ma to za zadanie ogłoszenie informacji o obsługiwanej sieci, takie jak znaczniki czasowe czy SSID. Interfejs podłączający skanuje kanały radiowe 802.11 w celu rejestracji takiej ramki. Ten parametr może wynosić od 100 do 60000 ms. Domyślnie przyjmuje wartość 100.
Należy pamięć o podawaniu odpowiedniej długości hasła do struktury. Przy WPA oraz WPA2 hasło musi się składać z minimum 8 znaków. W innym przypadku nie uda się uruchomić punktu dostępu.
Ustanowienie punktu dostępowego wykonuje się poprzez wywołanie następującej funkcji:
- void becomeAccessPoint(void)
- {
- /* disable default wifi logging */
- esp_log_level_set("wifi", ESP_LOG_NONE);
- /* create a new event group */
- wifi_event_group = xEventGroupCreate();
- /* Initialize tcpIp adapter, inside function it also initialize TCPIP stack */
- tcpip_adapter_init();
- /* stop DHCP server */
- ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP));
- /* Assign static ip to AP interface */
- tcpip_adapter_ip_info_t ipInfo;
- memset(&ipInfo, 0, sizeof(ipInfo));
- IP4_ADDR(&ipInfo.ip, 192,168,10,1);
- IP4_ADDR(&ipInfo.gw, 192,168,10,1);
- IP4_ADDR(&ipInfo.netmask, 255,255,255,0);
- ESP_ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &ipInfo));
- /* start DHCP server after disable it */
- ESP_ERROR_CHECK(tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP));
- /* Initialize wifi event handler, use when different types of events occur */
- ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, NULL));
- /* Initialize wifi stack as access point, config store in RAM */
- wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
- ESP_ERROR_CHECK(esp_wifi_init(&config));
- ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
- ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
- wifi_config_t apConfig = {
- .ap = {
- .ssid="WifiName",
- .ssid_len=0,
- .password="Password",
- .channel=0,
- .authmode=WIFI_AUTH_WPA2_PSK,
- .ssid_hidden=0,
- .max_connection=4,
- .beacon_interval=100
- }
- };
- ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &apConfig));
- /* start wifi */
- ESP_ERROR_CHECK(esp_wifi_start());
- }
Na początku uruchamia się nową grupę zdarzeń po czym uruchamia się adapter TCP IP z inicjalizacją stosu TCP IP. Następnie wyłączany jest serwer DHCP po czym wprowadzane jest Ip, brama oraz maska sieci do struktury tcpip_adapter_ip_info_t. Po zapisaniu danych DHCP jest ponownie uruchamiane. Dalej wykorzystywana jest funkcja uruchamiająca obsługę zdarzeń. W dalszej części ustawiane są parametry schowka czyli pamięci RAM gdzie będą przechowywane informacje. Po wprowadzeniu danych odnośnie konfiguracji punktu dostępu, ustawienia zostają zapisane. Na końcu uruchamiane jest wifi.
W przypadku nie podawania adresu IP jaki zostanie ustawiony powinien zostać wybrany automatycznie adres domyślny czyli: 192.168.4.1. Przy domyślnej konfiguracji można pominąć następujący fragment kodu:
Jeśli chce się uniknąć konieczności podawania hasła to należy zmienić parametr authmode w strukturze wifi_config_t na WIFI_AUTH_OPEN.
Przy pracy w trybie punktu dostępu występują trzy główne zdarzenia które należy skonfigurować. Wykonuje się to w funkcji obsługującej wszystkie zdarzenia:
- SYSTEM_EVENT_AP_START - zdarzenie generowane po uruchomieniu wifi.
- SYSTEM_EVENT_AP_STACONNECTED - kiedy urządzenia zostanie podłączone do punktu dostepu.
- SYSTEM_EVENT_AP_STADICONNECTED - wywołanie następuje w momencie rozłączenia urządzenia
Obsługa tych zdarzeń wygląda następująco:
- esp_err_t wifi_event_handler(void *ctx, system_event_t *event)
- {
- switch(event->event_id)
- {
- case SYSTEM_EVENT_AP_START:
- printf("SYSTEM_EVENT_AP_START...\n\n");
- break;
- case SYSTEM_EVENT_AP_STACONNECTED:
- printf("SYSTEM_EVENT_AP_STACONNECTED...\n\n");
- xEventGroupSetBits(wifi_event_group, BIT0);
- break;
- case SYSTEM_EVENT_AP_STADISCONNECTED:
- printf("SYSTEM_EVENT_AP_STADISCONNECTED...\n\n");
- xEventGroupSetBits(wifi_event_group, BIT1);
- break;
- default:
- break;
- }
- return ESP_OK;
- }
Zanim przejdę do strony internetowej przedstawię dwie funkcje pomocnicze. Ich zadaniem jest sprawdzanie czy udało się nawiązań połączenie z układem.
- void printConnectedStations()
- {
- printf("Display connected stations:\n");
- wifi_sta_list_t stations;
- tcpip_adapter_sta_list_t tcpip_adapter;
- memset(&wifi_sta_list, 0 ,sizeof(wifi_sta_list));
- memset(&tcpip_adapter, 0, sizeof(tcpip_adapter));
- ESP_ERROR_CHECK(esp_wifi_ap_get_sta_list(&stations));
- ESP_ERROR_CHECK(tcpip_adapter_get_sta_list(&stations, &tcpip_adapter));
- for(uint8_t i=0; i<tcpip_adapter.num; i++)
- {
- tcpip_adapter_sta_info_t sta_info = tcpip_adapter.sta[i];
- printf("Stat: %d - mac: %.2X:%.2X:%.2X:%.2X:%.2X:%.2X - IP: %s\n\n", i + 1,
- sta_info.mac[0], sta_info.mac[1], sta_info.mac[2], sta_info.mac[3],
- sta_info.mac[4], sta_info.mac[5], ip4addr_ntoa(&(sta_info.ip)));
- }
- ESP_ERROR_CHECK(esp_wifi_free_station_list());
- }
Funkcja powyżej wyświetla numer połączenia wraz z adresem mac oraz ip podłączonego urządzenia.
Kolejna funkcja udziela informacji czy wykryto nowe połączenie, bądź czy stare zostało wstrzymane:
- void displInfoAboutConnSta()
- {
- EventBits_t staBits = xEventGroupWaitBits(wifi_event_group, BIT0 | BIT1,
- pdTRUE, pdFALSE, portMAX_DELAY);
- if((staBits & BIT0) != 0)
- {
- printf("New device connected\n");
- }
- else
- {
- printf("A station disconnected\n");
- }
- }
Obie funkcje są wywoływane w osobnym zadaniu:
- void printfAPConnDev()
- {
- while(1)
- {
- displInfoAboutConnSta();
- printConnectedStations();
- vTaskDelay(10000/portTICK_RATE_MS);
- }
- }
Postawienie strony internetowej należy rozpocząć od podania nagłówka oraz zawartości strony:
Strona internetowa będzie generowana podczas wywołania odpowiedniego zadania:
- void http_webPage(void *pvParameters)
- {
- struct netconn *conn_net, *newconn;
- err_t err;
- printf("Set new server \n");
- /* Creates a new connection and returns a pointer to netconn struct */
- conn = netconn_new(NETCONN_TCP);
- netconn_bind(conn, IP_ADDR_ANY, 80);
- netconn_listen(conn);
- do
- {
- err = netconn_accept(conn, &newconn);
- if (err == ERR_OK)
- {
- http_server_netconn(newconn);
- netconn_delete(newconn);
- }
- } while(err == ERR_OK);
- netconn_close(conn);
- netconn_delete(conn);
- }
W pierwszej kolejności tworzone jest nowe połączenie, które zwraca strukturę z jego informacjami. Następnie należy połączyć stworzoną strukturę czyli połączenie z odpowiednim portem. Dla normalnej pracy jest to port 80. W przypadku korzystania z HTTPS będzie to już port 443. Aby połączyć się z urządzeniem na tym porcie należy wpisać adresIp:443.
Dalej następuje nasłuchiwanie na podanym porcie, po czym jeśli nastąpi połączenie nastąpi akceptacja połączenia. Czeka na żądanie połączenie od łączącego się klienta. Jeśli zwrócona zostanie informacja o poprawnym wykonaniu połączenia następuje wejście do funkcji obsługującej odebrane dane. Następnie struktura z informacjami o połączeniu jest usuwana a samo połączenie jest usuwane i nawiązywane ponownie w pętli. Po wyjściu z niej następuje zamknięcie oraz usunięcie stworzonego połączenia.
Następnie do przygotowania jest odpowiednia funkcja obsługująca dane jakie będą przesyłane ze strony do ESP:
- void http_server_netconn(struct netconn *conn)
- {
- struct netbuf *inbuf;
- char *buf;
- uint16_t buflen;
- err_t err;
- char data[20];
- /* Use method for receive data */
- err = netconn_recv(conn, &inbuf);
- printf("http_server_netconn_serve");
- if (err == ERR_OK)
- {
/* Use for getting a data */
- netbuf_data(inbuf, (void**)&buf, &buflen);
- /* Display buffer in the screen */
- printf("buffer = %s \n", buf);
- /* Check if there was any GET */
- if(buflen>=5 && buf[0]=='G' && buf[1]=='E' && buf[2]=='T' && buf[3]==' ' && buf[4]=='/' )
- {
- printf("buf[5] = %c \n", buf[5]);
- /*
- * Send the HTML header
- */
- printf("netconn_write \n\n");
/* Write data to client */
- netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY);
- netconn_write(conn, http_index_hml, sizeof(http_index_hml)-1, NETCONN_NOCOPY);
- }
- /* If there was a post msg */
- else if(buf[0] == 'P' && buf[1] == 'O' && buf[2] == 'S' && buf[3] == 'T' && buf[4]==' ' && buf[5]=='/')
- {
- Uart_Send_Data(1, (uint8_t*)"Data receive\r\n");
- Uart_Send_Data(1, (uint8_t*)buf);
- char *ptr = strstr(buf, "ssid=");
- if(ptr)
- {
- Uart_Send_Data(1, (uint8_t*)"found ssid\r\n");
- /* Decode receive msg */
- decodeValues(ptr, 200);
- }
- netconn_write(conn, http_index_post_enterData, sizeof(http_index_post_enterData)-1, NETCONN_NOCOPY);
- }
- }
- /* Close the connection (server closes in HTTP) */
- netconn_close(conn);
- /* Delete buffer from netbuf */
- netbuf_delete(inbuf);
- }
Ta funkcja w przypadku otrzymania komunikatu post ładuje nową stronę w przeglądarce. W przypadku metody get zostaje wysłana ramka danych, która jest dekodowana w dwóch funkcjach. Pierwsza z nich:
- decodeValues(char* buffer, uint16_t bufferLength);
Dostaje odebrany bufor danych, już wyczyszczony z niepotrzebnych elementów oraz jego długość. W tym przypadku funkcja odzyskuje parametry sieci takie jak jej nazwa, hasło, adres IP, bramę domyślną, maskę podsieci.
Data oraz godzina są wyciągane z bufora poprzez funkcję:
- static void decodeDatTimeBuf(char *dateTime);
Data oraz godzina wprowadzone do rubryki muszą być w formacie dd.mm.rr-gg.mm. Funkcja po odczycie wprowadza dane do układu RTC komunikującego się poprzez I2C (DS3231).
Cały odebrany bufor od strony internetowej wygląda następująco:
Jeśli o dane o sieci to po ich pobraniu są one konwertowane oraz zapisywane w pamięci układu:
- typedef struct{
- char ssid[SSID_SIZE];
- char password[PASSWORD_SIZE];
- tcpip_adapter_ip_info_t ipInfo;
- }connection_info_t;
- static void writeDataToConnectionInfoStruct(char *SSID, char *Pass, char *Ip, char *gateWay, char *netMask, char *dateTime)
- {
- char data[70];
- /* Write SSID to main buffer */
- for(uint8_t loop = 0; loop<32; loop++) { connectionInfo.ssid[loop] = SSID[loop]; }
- /* Write password to main buffer */
- for(uint8_t loop = 0; loop<64; loop++) { connectionInfo.password[loop] = Pass[loop]; }
- if(strcmp(Ip, "") != 0) { inet_pton(AF_INET, Ip, &connectionInfo.ipInfo.ip); }
- else { connectionInfo.ipInfo.ip.addr = 0; }
- if(strcmp(gateWay, "") != 0) { inet_pton(AF_INET, gateWay, &connectionInfo.ipInfo.gw); }
- else { connectionInfo.ipInfo.gw.addr = 0; }
- if(strcmp(netMask, "") != 0) { inet_pton(AF_INET, netMask, &connectionInfo.ipInfo.netmask); }
- else { connectionInfo.ipInfo.netmask.addr = 0; }
- /* write data into I2C clock */
- if(strcmp(dateTime, "") != 0) { decodeDatTimeBuf(dateTime); }
- saveConnectionInfo(&connectionInfo);
- }
- static void saveConnectionInfo(connection_info_t *pConnectionInfo)
- {
- nvs_handle handle;
- ESP_ERROR_CHECK(nvs_open(BOOTWIFI_NAMESPACE, NVS_READWRITE, &handle));
- ESP_ERROR_CHECK(nvs_set_blob(handle, KEY_CONNECTION_INFO, pConnectionInfo, sizeof(connection_info_t)));
- ESP_ERROR_CHECK(nvs_set_u32(handle, KEY_VERSION, g_version));
- ESP_ERROR_CHECK(nvs_commit(handle));
- nvs_close(handle);
- }
Zdarzenie obsługujące stronę internetową wygląda następująco:
- esp_err_t wifi_event_handler(void *ctx, system_event_t *event)
- {
- printf("wifi_event_handler...\n\n");
- switch(event->event_id)
- {
- /* --------------- Access point stats --------------- */
- case SYSTEM_EVENT_AP_START:
- printf("SYSTEM_EVENT_AP_START...\n\n");
- break;
- case SYSTEM_EVENT_AP_STACONNECTED:
- printf("SYSTEM_EVENT_AP_STACONNECTED...\n\n");
- xEventGroupSetBits(wifi_event_group, BIT0);
- break;
- case SYSTEM_EVENT_AP_STADISCONNECTED:
- printf("SYSTEM_EVENT_AP_STADISCONNECTED...\n\n");
- xEventGroupSetBits(wifi_event_group, BIT1);
- break;
- default:
- break;
- }
- printf("wifi_event_handler exit...\n\n");
- return ESP_OK;
- }
U mnie w programie obsługuje dodatkowo zegar po I2C oraz dwa UARTy. Wobec tego dla samego wifi należy użyć tylko dwóch funkcji:
- void app_main(void)
- {
- nvs_flash_init();
- becomeAccessPoint();
- xTaskCreate(&http_server, "http_server", 2048, NULL, 5 NULL);
- }
Bibliotekę do połączenia statycznego można pobrać pod tym linkiem w zakładce ESP32. Znajdują się tam także kody źródłowe przykładowej strony internetowej. Jeśli do tego wykorzystuje się UART to należy pamiętać o wcześniejszym uruchomieniu układów do niego. W przeciwnym wypadku nastąpi reset ESP.