wtorek, 8 maja 2018

[1] ESP32 - Arduino - Logowanie do wifi

W tym poście chciałbym przedstawić sposób wykonania serwera HTTP jako AP, który pozwoli na logowanie do sieci Wifi. Po zalogowaniu ssid oraz hasło będą przechowywane w pamięci NVS, a układ ESP32 będzie skonfigurowany jako serwer TCP. Odebrane ramki danych będą przesyłane przez UART.

Znalezione obrazy dla zapytania arduino esp32
[Źródło: http://paulobrien.co.nz/2017/03/16/esp32-programming-with-arduino-on-windows/]

Program:


Poniżej przejdę przez poszczególne funkcje.

Do projektu należy dołączyć biblioteki Wifi do obsługi połączeń sieciowych oraz Preferences do NVS.

  1. #include <WiFi.h>
  2. #include <Preferences.h>

Obsługa wiadomości do debugowania została wykonana na następujących funkcjach:

  1. /* Debug settings */
  2. #define ENABLE_DEBUG            1
  3. #define UART_DEBUG_SPEED        115200
  4. void enableSerialPortDebug()
  5. {
  6.   #if ENABLE_DEBUG == 1
  7.   Serial.begin(UART_DEBUG_SPEED);
  8.   while(!Serial) { ; } SERIAL_DEBUG_MSG("Debug UART Enable....", true);
  9.   #endif
  10. }
  11. void SERIAL_DEBUG_MSG(const char *dataToSend, bool newLine) {
  12.   #if ENABLE_DEBUG == 1
  13.   if(newLine == true) {   Serial.println(dataToSend);   }
  14.   else                {   Serial.print(dataToSend);     }
  15.   #endif
  16. }
  17. void SERIAL_DEBUG_MSG_VALUE(const char dataToDisplay) {
  18.   #if ENABLE_DEBUG == 1
  19.   Serial.print(dataToDisplay);  
  20.   #endif
  21. }

Jeśli ENABLE_DEBUG będzie ustawiony na 1 to wiadomości tekstowe przez port szeregowy będą wysyłane do użytkownika.

Funkcja uruchamiająca wszystkie elementy:

  1. void setup()
  2. {
  3.   enableSerialPortDebug();
  4.   NonVolatileStorageRead();
  5.   enableAPWifi();
  6.   enableWifiDisplaInfo();
  7.   setHttpOrTcpServer();
  8.   enableGPIOInterrupt();
  9. }

Pętla główna programu wygląda następująco:

  1. void loop()
  2. {    
  3.   if(wifi_connected == false)
  4.   {
  5.     apHttp();
  6.   }
  7.   else
  8.   {
  9.     wifiTCPServer_Loop();
  10.   }
  11.   checkIfInterruptOccure();
  12. }

Teraz funkcje obsługujące przerwanie:

Uruchomienie przerwania od podanego pinu:

  1. void enableGPIOInterrupt(void){
  2.   pinMode(interruptPin, INPUT_PULLUP);
  3.   attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING);
  4. }

Funkcja powyżej uruchamia pin z podciągnięciem do zasilania, a następnie przypisuje do niego przerwanie na zboczu opadającym. Obsługa przerwania odbywa się w funkcji handleInterrupt.

  1. portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
  2. void IRAM_ATTR handleInterrupt() {
  3.   portENTER_CRITICAL_ISR(&mux);
  4.   interruptBtnChanEntExt = true;
  5.   portEXIT_CRITICAL_ISR(&mux);
  6. }

Tutaj potrzebna jest zmienna typu portMUX_TYPE, która jest potrzebna do synchronizacji pomiędzy głównym kodem programu oraz przerwaniami. Zmiana zmiennej informującej o wystąpieniu przerwania jest otoczona informacją o krytycznym miejscu w programie.

Atrybut IRAM_ATTR umieszcza kod obsługi przerwania w pamięci IRAM. Dodatkowo należy pamiętać, że wywołanie obsługi przerwania z pamięci IRAM wymaga, aby funkcje wywołane w przerwaniu także się w niej znajdowały.

W głównej pętli wykonuję obsługę przerwania, która wygląda następująco:

  1. void checkIfInterruptOccure(void){
  2.   if(interruptBtnChanEntExt == true){
  3.     SERIAL_DEBUG_MSG("Interrupt From Btn, Clear Settings"true);
  4.     wifiSSID = "none";
  5.     wifiPassword = "none";
  6.     NonVolatileStorageWrite();
  7.    
  8.     portENTER_CRITICAL(&mux);
  9.     interruptBtnChanEntExt = false;
  10.     portEXIT_CRITICAL(&mux);
  11.     delay(1000);
  12.     ESP.restart();
  13.   }
  14. }

Gdy pojawi się przerwanie to czyszczę zmienne zawierające informacje o sieci w NVS, po czym zeruje zmienną informującą o wywołaniu przerwania (to w zasadzie można w tym przypadku pominąć). Na samym końcu czekam około 1 sekundy po czym wykonuje restart ESP. Po ponownym załadowaniu ESP uruchomi się jako AP.

Aby uzyskać dostęp do NVS posługuje się dwiema funkcjami:

  1. void NonVolatileStorageRead(void)
  2. {
  3.   preferences.begin("wifi"false);
  4.   wifiSSID = preferences.getString("ssid""none");          
  5.   wifiPassword = preferences.getString("password""none");
  6.   preferences.end();
  7. }
  8. void NonVolatileStorageWrite(void)
  9. {
  10.   preferences.clear();
  11.   preferences.begin("wifi"false);
  12.   preferences.putString("ssid", ssidData);
  13.   preferences.putString("password", passwordData);
  14.   preferences.end();
  15. }

Jedna służy do pobrania danych o sieci, druga natomiast wpisuje te informacje do pamięci.

Teraz uruchomienie sieci:

  1. void enableAPWifi(void)
  2. {
  3.   SERIAL_DEBUG_MSG(" "true);
  4.   SERIAL_DEBUG_MSG(" "true);
  5.   SERIAL_DEBUG_MSG("Connecting to "false);
  6.   SERIAL_DEBUG_MSG(ssid, true);
  7.   WiFi.onEvent(WiFiEvent);
  8.   WiFi.mode(WIFI_MODE_APSTA);
  9.   WiFi.softAP(ssid, password);
  10.   SERIAL_DEBUG_MSG("AP Started"true);
  11.   SERIAL_DEBUG_MSG("AP SSID: "false);
  12.   SERIAL_DEBUG_MSG(ssid, true);
  13.   SERIAL_DEBUG_MSG("AP IPv4: "false);
  14.   Serial.println(WiFi.softAPIP());  
  15. }

Po przesłaniu informacji do użytkownika następuje uruchomienie zdarzeń dla sieci wifi, po czym układ jest uruchamiany jako AP. Gdy występują dane dotyczące sieci układ przechodzi do podłączenia.

Obsługa zdarzeń:

  1. void WiFiEvent(WiFiEvent_t event){
  2.   switch (event) {
  3.     case SYSTEM_EVENT_AP_START:
  4.       WiFi.softAP(ssid, password);
  5.       WiFi.softAPenableIpV6();
  6.       break;
  7.     case SYSTEM_EVENT_STA_START:
  8.       WiFi.setHostname(ssid);
  9.       break;
  10.     case SYSTEM_EVENT_STA_CONNECTED:
  11.       WiFi.enableIpV6();
  12.       break;
  13.     case SYSTEM_EVENT_AP_STA_GOT_IP6:
  14.       Serial.print("STA IPv6: ");
  15.       Serial.println(WiFi.localIPv6());
  16.       Serial.print("AP IPv6: ");
  17.       Serial.println(WiFi.softAPIPv6());
  18.       break;
  19.     case SYSTEM_EVENT_STA_GOT_IP:
  20.       wifiOnConnect();
  21.       wifi_connected = true;
  22.       break;
  23.     case SYSTEM_EVENT_STA_DISCONNECTED:
  24.       wifi_connected = false;
  25.       wifiOnDisconnect();
  26.       break;
  27.     default:
  28.       break;
  29.   }
  30. }

Tutaj w zależności od wywołanego zdarzenia uruchamiane są odpowiednie funkcje zajmujące się ich obsługą.

Gdy uda się połączyć do sieci:

  1. void wifiOnConnect()
  2. {
  3.   SERIAL_DEBUG_MSG("STAND CONNECTED - SSID: "false);
  4.   Serial.println(WiFi.SSID());
  5.   SERIAL_DEBUG_MSG("STAND IPv4: "false);
  6.   Serial.println(WiFi.localIP());
  7.   Serial.print("STAND IPv6: ");
  8.   Serial.println(WiFi.localIPv6());
  9.   WiFi.mode(WIFI_MODE_STA);
  10. }

Tutaj wyświetlam komunikaty dla użytkownika oraz ustawiam wifi w tryb STA.

  1. void wifiOnDisconnect()
  2. {
  3.   SERIAL_DEBUG_MSG("STAND Disconnected"true);
  4.   delay(500);
  5.   WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
  6. }

W przypadku odłączeniu wyświetla się wiadomość dla użytkownika po czym następuje próba ponownego połączenia.

Obsługa sieci w pętli głównej dzieli się na serwer HTTP dla AP oraz serwer TCP dla STA.

W zależności od aktualnego trybu pracy uruchamiany jest odpowiedni serwer:

  1. void setHttpOrTcpServer(void)
  2. {
  3.   if(wifiSSID == "")
  4.   {
  5.     SERIAL_DEBUG_MSG("Wifi as AP"true);
  6.     server.begin();
  7.   }
  8.   else
  9.   {
  10.     SERIAL_DEBUG_MSG("Tcp server Enable"true);
  11.     tcpServer.begin();
  12.   }
  13. }

Ładowanie strony internetowej:

  1. void apHttp(void)
  2. {
  3.     WiFiClient client = server.available();       // listen for incoming clients
  4.     if (client)
  5.     {
  6.       SERIAL_DEBUG_MSG("New client",true);
  7.       memset(dataBuffer,0,sizeof(dataBuffer));
  8.       countChars=0;
  9.       boolean currentLineIsBlank = true;
  10.      
  11.       while (client.connected())
  12.       {
  13.         if (client.available())
  14.         {
  15.           char c = client.read();
  16.           Serial.write(c);
  17.          
  18.           dataBuffer[countChars]=c;
  19.           if (countChars<sizeof(dataBuffer)-1) {
  20.             countChars++;
  21.           }
  22.          
  23.           if (== '\n' && currentLineIsBlank)
  24.           {
  25.             SERIAL_DEBUG_MSG("Send response"true);
  26.            
  27.             displayWebPage(&client);
  28.             break;
  29.           }
  30.          
  31.           if (== '\n')
  32.           {
  33.             client.println("HTTP/1.1 200 OK");
  34.             currentLineIsBlank = true;
  35.            
  36.             if (strContain(dataBuffer, "rec_data"))
  37.             {
  38.               client.println("Content-Type: text/xml");
  39.               client.println("Connection: keep-alive");
  40.               client.println();
  41.              
  42.               if (getSSIDPasswData(ssidData, passwordData, SSID_BUFFER_SIZE))
  43.               {
  44.                 wifiSSID = ssidData;
  45.                 wifiPassword = passwordData;
  46.                 SERIAL_DEBUG_MSG("############################"true);
  47.                 Serial.println(wifiSSID);
  48.                 Serial.println(wifiPassword);
  49.                 SERIAL_DEBUG_MSG("############################"true);
  50.                 NonVolatileStorageWrite();
  51.                 // the content of the HTTP response follows the header:
  52.                 client.print("<h1>OK! Restarting in 5 seconds...</h1>");
  53.                 client.println();
  54.                 Serial.println("Data sended, now restarting");
  55.                 delay(4000);
  56.                 ESP.restart();
  57.               }
  58.             }
  59.             currentLineIsBlank = true;
  60.             memset(dataBuffer,0,sizeof(dataBuffer));
  61.             countChars=0;
  62.           }
  63.           else if (!= '\r') {
  64.             currentLineIsBlank = false;
  65.           }
  66.         }
  67.       }
  68.       // give the web browser time to receive the data
  69.       delay(1);
  70.       // close the connection:
  71.       client.stop();
  72.       SERIAL_DEBUG_MSG("client disconnected"true);
  73.     }
  74. }

Powyżej obsługuje nowe połączenie po czym wyświetla stronę internetową. Gdy przyjdzie odpowiedź to następuje wprowadzenie danych oraz restart ESP.

Załadowanie strony internetowej wygląda następująco:

  1. void displayWebPage(WiFiClient *client)
  2. {
  3.   client->println("HTTP/1.1 200 OK");
  4.   client->println("Content-Type: text/html");
  5.   client->println("Connection: close");  // the connection will be closed after completion of the response
  6.   client->println();
  7.   client->println("<!DOCTYPE HTML><html><head>");
  8.   client->println("<body bgcolor=\"#E6E6FA\">");
  9.   client->println("<meta charset=\"utf-8\">");
  10.   client->println("<script>");
  11.   client->println("ssidInfo = \"\";");
  12.   client->println("passInfo = \"\";");
  13.   client->println("function SendText()");
  14.   client->println("{");
  15.   client->println("nocache = \"&nocache=\" + Math.random() * 1000000;");
  16.   client->println("var request = new XMLHttpRequest();");
  17.   client->println("ssidInfo = \"&s1=\" + document.getElementById(\"txt_form\").ssidData.value;");
  18.   client->println("passInfo = \"&p1=\" + document.getElementById(\"txt_form\").passData.value;");
  19.   client->println("request.open(\"GET\"\"rec_data\" + ssidInfo + passInfo + nocache, true);");
  20.   client->println("request.send(null);");
  21.   client->println("}");
  22.   client->println("</script>");
  23.   client->println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></head>");
  24.   client->println("<h1>Ustaw wifi</h1>");
  25.   client->println("<body onload=\"WriteData\">");
  26.   client->println("<p><b>Wprowadź dane do logowania:</b></p>");
  27.   client->println("<form id=\"txt_form\" name=\"frmText\">");
  28.   client->println("<label>SSID: <input type=\"text\" name=\"ssidData\" size=\"32\" maxlength=\"32\" /></label><br /><br />");
  29.   client->println("<label>Hasło: <input type=\"text\" name=\"passData\" size=\"64\" maxlength=\"64\" /></label>");
  30.   client->println("</form>");
  31.   client->println("<br />");
  32.   client->println("<input type=\"submit\" value=\"Zapisz Ustawienia\" onclick=\"SendText()\" />");
  33.   client->println("</body>");
  34.   client->println("</html>");
  35. }

Do niej przekazywany jest wskaźnik do połączenie gdzie przekazywany jest cały kod HTML.

Cały projekt można pobrać z dysku Google pod tym linkiem.