środa, 8 listopada 2017

[8] ESP32 - Uruchomienie punktu dostępu do wifi z serwerem HTTP

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.

[Źródło: www.banggood.com]

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:

  1. void becomeAccessPoint(void)
  2. {
  3.     /* disable default wifi logging */
  4.     esp_log_level_set("wifi", ESP_LOG_NONE);
  5.     
  6.     /* create a new event group */
  7.     wifi_event_group = xEventGroupCreate();
  8.     /* Initialize tcpIp adapter, inside function it also initialize TCPIP stack */
  9.     tcpip_adapter_init();
  10.     /* stop DHCP server */
  11.     ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP));
  12.     /* Assign static ip to AP interface */
  13.     tcpip_adapter_ip_info_t ipInfo;
  14.     memset(&ipInfo, 0, sizeof(ipInfo));
  15.     IP4_ADDR(&ipInfo.ip, 192,168,10,1);
  16.     IP4_ADDR(&ipInfo.gw, 192,168,10,1);
  17.     IP4_ADDR(&ipInfo.netmask, 255,255,255,0);
  18.     ESP_ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &ipInfo));
  19.     /* start DHCP server after disable it */
  20.     ESP_ERROR_CHECK(tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP));
  21.     /* Initialize wifi event handler, use when different types of events occur */
  22.     ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, NULL));
  23.     /* Initialize wifi stack as access point, config store in RAM */
  24.     wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
  25.     ESP_ERROR_CHECK(esp_wifi_init(&config));                     
  26.     ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
  27.     ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
  28.     wifi_config_t apConfig = {
  29.             .ap = {
  30.                     .ssid="WifiName",
  31.                     .ssid_len=0,
  32.                     .password="Password",
  33.                     .channel=0,
  34.                     .authmode=WIFI_AUTH_WPA2_PSK,
  35.                     .ssid_hidden=0,
  36.                     .max_connection=4,
  37.                     .beacon_interval=100
  38.             }
  39.     };
  40.     ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &apConfig));
  41.     /* start wifi */
  42.     ESP_ERROR_CHECK(esp_wifi_start());
  43. }

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:

  1. esp_err_t wifi_event_handler(void *ctx, system_event_t *event)
  2. {
  3.     switch(event->event_id)
  4.     {
  5.         case SYSTEM_EVENT_AP_START:
  6.             printf("SYSTEM_EVENT_AP_START...\n\n");
  7.         break;
  8.         case SYSTEM_EVENT_AP_STACONNECTED:
  9.             printf("SYSTEM_EVENT_AP_STACONNECTED...\n\n");
  10.             xEventGroupSetBits(wifi_event_group, BIT0);
  11.         break;
  12.         case SYSTEM_EVENT_AP_STADISCONNECTED:
  13.             printf("SYSTEM_EVENT_AP_STADISCONNECTED...\n\n");
  14.             xEventGroupSetBits(wifi_event_group, BIT1);
  15.         break;
  16.         default:
  17.         break;
  18.     }
  19.     return ESP_OK;
  20. }

Zanim przejdę do strony internetowej przedstawię dwie funkcje pomocnicze. Ich zadaniem jest sprawdzanie czy udało się nawiązań połączenie z układem.

  1. void printConnectedStations()
  2. {
  3.     printf("Display connected stations:\n");
  4.     wifi_sta_list_t stations;
  5.     tcpip_adapter_sta_list_t tcpip_adapter;
  6.     memset(&wifi_sta_list, 0 ,sizeof(wifi_sta_list));
  7.     memset(&tcpip_adapter, 0, sizeof(tcpip_adapter));
  8.     
  9.     ESP_ERROR_CHECK(esp_wifi_ap_get_sta_list(&stations));
  10.     ESP_ERROR_CHECK(tcpip_adapter_get_sta_list(&stations, &tcpip_adapter));
  11.     for(uint8_t i=0; i<tcpip_adapter.num; i++)
  12.     {
  13.         tcpip_adapter_sta_info_t sta_info = tcpip_adapter.sta[i];
  14.         printf("Stat: %d - mac: %.2X:%.2X:%.2X:%.2X:%.2X:%.2X - IP: %s\n\n", i + 1,
  15.                 sta_info.mac[0], sta_info.mac[1], sta_info.mac[2], sta_info.mac[3],
  16.                 sta_info.mac[4], sta_info.mac[5], ip4addr_ntoa(&(sta_info.ip)));
  17.     }
  18.     ESP_ERROR_CHECK(esp_wifi_free_station_list());
  19. }

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:

  1. void displInfoAboutConnSta()
  2. {
  3.     EventBits_t staBits = xEventGroupWaitBits(wifi_event_group, BIT0 | BIT1,
  4.                                             pdTRUE, pdFALSE, portMAX_DELAY);
  5.     if((staBits & BIT0) != 0)
  6.     {
  7.         printf("New device connected\n");
  8.     }
  9.     else
  10.     {
  11.         printf("A station disconnected\n");
  12.     }
  13. }

Obie funkcje są wywoływane w osobnym zadaniu:

  1. void printfAPConnDev()
  2. {
  3.     while(1)
  4.     {
  5.         displInfoAboutConnSta();
  6.         printConnectedStations();
  7.         vTaskDelay(10000/portTICK_RATE_MS);
  8.     }
  9. }

Postawienie strony internetowej należy rozpocząć od podania nagłówka oraz zawartości strony:

Strona internetowa będzie generowana podczas wywołania odpowiedniego zadania:

  1. void http_webPage(void *pvParameters)
  2. {
  3.   struct netconn *conn_net, *newconn;
  4.   err_t err;
  5.   printf("Set new server \n");
  6.   /* Creates a new connection and returns a pointer to netconn struct */
  7.   conn = netconn_new(NETCONN_TCP);
  8.   netconn_bind(conn, IP_ADDR_ANY, 80);
  9.   netconn_listen(conn);
  10.   do
  11.   {
  12.      err = netconn_accept(conn, &newconn);
  13.      if (err == ERR_OK)
  14.      {
  15.        http_server_netconn(newconn);
  16.        netconn_delete(newconn);
  17.      }
  18.    } while(err == ERR_OK);
  19.   netconn_close(conn);
  20.   netconn_delete(conn);
  21. }

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:

  1. void http_server_netconn(struct netconn *conn)
  2. {
  3.   struct netbuf *inbuf;
  4.   char *buf;
  5.   uint16_t buflen;
  6.   err_t err;
  7.   char data[20];
  8.   /* Use method for receive data */
  9.   err = netconn_recv(conn, &inbuf);
  10.   printf("http_server_netconn_serve");
  11.   if (err == ERR_OK)
  12.   {
  13. /* Use for getting a data */
  14.     netbuf_data(inbuf, (void**)&buf, &buflen);
  15.     /* Display buffer in the screen */
  16.     printf("buffer = %s \n", buf);
  17.     /* Check if there was any GET */
  18.     if(buflen>=5 && buf[0]=='G' && buf[1]=='E' && buf[2]=='T' && buf[3]==' ' && buf[4]=='/' )
  19.     {
  20.         printf("buf[5] = %c \n", buf[5]);
  21.        /*
  22.         * Send the HTML header
  23.         */
  24.        
  25.         printf("netconn_write \n\n");

  26.  /* Write data to client */
  27.         netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY);
  28.         netconn_write(conn, http_index_hml, sizeof(http_index_hml)-1, NETCONN_NOCOPY);
  29.     }
  30.     /* If there was a post msg */
  31.     else if(buf[0] == 'P' && buf[1] == 'O' && buf[2] == 'S' && buf[3] == 'T' && buf[4]==' ' && buf[5]=='/')
  32.     {
  33.         Uart_Send_Data(1, (uint8_t*)"Data receive\r\n");
  34.         Uart_Send_Data(1, (uint8_t*)buf);
  35.         char *ptr = strstr(buf, "ssid=");
  36.         if(ptr)
  37.         {
  38.             Uart_Send_Data(1, (uint8_t*)"found ssid\r\n");
  39.             /* Decode receive msg */
  40.             decodeValues(ptr, 200);
  41.         }
  42.         netconn_write(conn, http_index_post_enterData, sizeof(http_index_post_enterData)-1, NETCONN_NOCOPY);
  43.     }
  44.   }
  45.   /* Close the connection (server closes in HTTP) */
  46.   netconn_close(conn);
  47.   /* Delete buffer from netbuf */
  48.   netbuf_delete(inbuf);
  49. }

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:

  1. 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ę:

  1. 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:

  1. typedef struct{
  2.     char ssid[SSID_SIZE];
  3.     char password[PASSWORD_SIZE];
  4.     tcpip_adapter_ip_info_t ipInfo;
  5. }connection_info_t;
  6. static void writeDataToConnectionInfoStruct(char *SSID, char *Pass, char *Ip, char *gateWay, char *netMask, char *dateTime)
  7. {
  8.     char data[70];
  9.     /* Write SSID to main buffer */
  10.     for(uint8_t loop = 0; loop<32; loop++)  {   connectionInfo.ssid[loop] = SSID[loop];     }
  11.     /* Write password to main buffer */
  12.     for(uint8_t loop = 0; loop<64; loop++)  {   connectionInfo.password[loop] = Pass[loop]; }
  13.     if(strcmp(Ip, "") != 0)                 {   inet_pton(AF_INET, Ip, &connectionInfo.ipInfo.ip);              }
  14.     else                                    {   connectionInfo.ipInfo.ip.addr = 0;                              }
  15.     if(strcmp(gateWay, "") != 0)            {   inet_pton(AF_INET, gateWay, &connectionInfo.ipInfo.gw);         }
  16.     else                                    {   connectionInfo.ipInfo.gw.addr = 0;                              }
  17.     if(strcmp(netMask, "") != 0)            {   inet_pton(AF_INET, netMask, &connectionInfo.ipInfo.netmask);    }
  18.     else                                    {   connectionInfo.ipInfo.netmask.addr = 0;                         }
  19.     /* write data into I2C clock */
  20.     if(strcmp(dateTime, "") != 0)           { decodeDatTimeBuf(dateTime); }
  21.    
  22.     saveConnectionInfo(&connectionInfo);
  23. }
  24. static void saveConnectionInfo(connection_info_t *pConnectionInfo)
  25. {
  26.     nvs_handle handle;
  27.     ESP_ERROR_CHECK(nvs_open(BOOTWIFI_NAMESPACE, NVS_READWRITE, &handle));
  28.     ESP_ERROR_CHECK(nvs_set_blob(handle, KEY_CONNECTION_INFO, pConnectionInfo, sizeof(connection_info_t)));
  29.     ESP_ERROR_CHECK(nvs_set_u32(handle, KEY_VERSION, g_version));
  30.     ESP_ERROR_CHECK(nvs_commit(handle));
  31.     nvs_close(handle);
  32. }

Zdarzenie obsługujące stronę internetową wygląda następująco:

  1. esp_err_t wifi_event_handler(void *ctx, system_event_t *event)
  2. {
  3.     printf("wifi_event_handler...\n\n");
  4.     switch(event->event_id)
  5.     {
  6.     /* --------------- Access point stats --------------- */
  7.     case SYSTEM_EVENT_AP_START:
  8.         printf("SYSTEM_EVENT_AP_START...\n\n");
  9.         break;
  10.     case SYSTEM_EVENT_AP_STACONNECTED:
  11.         printf("SYSTEM_EVENT_AP_STACONNECTED...\n\n");
  12.         xEventGroupSetBits(wifi_event_group, BIT0);
  13.         break;
  14.     case SYSTEM_EVENT_AP_STADISCONNECTED:
  15.         printf("SYSTEM_EVENT_AP_STADISCONNECTED...\n\n");
  16.         xEventGroupSetBits(wifi_event_group, BIT1);
  17.         break;
  18.     default:
  19.         break;
  20.     }
  21.     printf("wifi_event_handler exit...\n\n");
  22.     return ESP_OK;
  23. }

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:

  1. void app_main(void)
  2. {
  3.     nvs_flash_init();
  4.     becomeAccessPoint();
  5.     xTaskCreate(&http_server, "http_server", 2048, NULL, 5 NULL);
  6. }

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.