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.
[Ź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.
- #include <WiFi.h>
- #include <Preferences.h>
Obsługa wiadomości do debugowania została wykonana na następujących funkcjach:
- /* Debug settings */
- #define ENABLE_DEBUG 1
- #define UART_DEBUG_SPEED 115200
- void enableSerialPortDebug()
- {
- #if ENABLE_DEBUG == 1
- Serial.begin(UART_DEBUG_SPEED);
- while(!Serial) { ; } SERIAL_DEBUG_MSG("Debug UART Enable....", true);
- #endif
- }
- void SERIAL_DEBUG_MSG(const char *dataToSend, bool newLine) {
- #if ENABLE_DEBUG == 1
- if(newLine == true) { Serial.println(dataToSend); }
- else { Serial.print(dataToSend); }
- #endif
- }
- void SERIAL_DEBUG_MSG_VALUE(const char dataToDisplay) {
- #if ENABLE_DEBUG == 1
- Serial.print(dataToDisplay);
- #endif
- }
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:
- void setup()
- {
- enableSerialPortDebug();
- NonVolatileStorageRead();
- enableAPWifi();
- enableWifiDisplaInfo();
- setHttpOrTcpServer();
- enableGPIOInterrupt();
- }
Pętla główna programu wygląda następująco:
- void loop()
- {
- if(wifi_connected == false)
- {
- apHttp();
- }
- else
- {
- wifiTCPServer_Loop();
- }
- checkIfInterruptOccure();
- }
Teraz funkcje obsługujące przerwanie:
Uruchomienie przerwania od podanego pinu:
- void enableGPIOInterrupt(void){
- pinMode(interruptPin, INPUT_PULLUP);
- attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING);
- }
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.
- portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
- void IRAM_ATTR handleInterrupt() {
- portENTER_CRITICAL_ISR(&mux);
- interruptBtnChanEntExt = true;
- portEXIT_CRITICAL_ISR(&mux);
- }
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:
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:
Jedna służy do pobrania danych o sieci, druga natomiast wpisuje te informacje do pamięci.
Teraz uruchomienie sieci:
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ń:
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:
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:
- void checkIfInterruptOccure(void){
- if(interruptBtnChanEntExt == true){
- SERIAL_DEBUG_MSG("Interrupt From Btn, Clear Settings", true);
- wifiSSID = "none";
- wifiPassword = "none";
- NonVolatileStorageWrite();
- portENTER_CRITICAL(&mux);
- interruptBtnChanEntExt = false;
- portEXIT_CRITICAL(&mux);
- delay(1000);
- ESP.restart();
- }
- }
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:
- void NonVolatileStorageRead(void)
- {
- preferences.begin("wifi", false);
- wifiSSID = preferences.getString("ssid", "none");
- wifiPassword = preferences.getString("password", "none");
- preferences.end();
- }
- void NonVolatileStorageWrite(void)
- {
- preferences.clear();
- preferences.begin("wifi", false);
- preferences.putString("ssid", ssidData);
- preferences.putString("password", passwordData);
- preferences.end();
- }
Jedna służy do pobrania danych o sieci, druga natomiast wpisuje te informacje do pamięci.
Teraz uruchomienie sieci:
- void enableAPWifi(void)
- {
- SERIAL_DEBUG_MSG(" ", true);
- SERIAL_DEBUG_MSG(" ", true);
- SERIAL_DEBUG_MSG("Connecting to ", false);
- SERIAL_DEBUG_MSG(ssid, true);
- WiFi.onEvent(WiFiEvent);
- WiFi.mode(WIFI_MODE_APSTA);
- WiFi.softAP(ssid, password);
- SERIAL_DEBUG_MSG("AP Started", true);
- SERIAL_DEBUG_MSG("AP SSID: ", false);
- SERIAL_DEBUG_MSG(ssid, true);
- SERIAL_DEBUG_MSG("AP IPv4: ", false);
- Serial.println(WiFi.softAPIP());
- }
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ń:
- void WiFiEvent(WiFiEvent_t event){
- switch (event) {
- case SYSTEM_EVENT_AP_START:
- WiFi.softAP(ssid, password);
- WiFi.softAPenableIpV6();
- break;
- case SYSTEM_EVENT_STA_START:
- WiFi.setHostname(ssid);
- break;
- case SYSTEM_EVENT_STA_CONNECTED:
- WiFi.enableIpV6();
- break;
- case SYSTEM_EVENT_AP_STA_GOT_IP6:
- Serial.print("STA IPv6: ");
- Serial.println(WiFi.localIPv6());
- Serial.print("AP IPv6: ");
- Serial.println(WiFi.softAPIPv6());
- break;
- case SYSTEM_EVENT_STA_GOT_IP:
- wifiOnConnect();
- wifi_connected = true;
- break;
- case SYSTEM_EVENT_STA_DISCONNECTED:
- wifi_connected = false;
- wifiOnDisconnect();
- break;
- default:
- break;
- }
- }
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:
- void wifiOnConnect()
- {
- SERIAL_DEBUG_MSG("STAND CONNECTED - SSID: ", false);
- Serial.println(WiFi.SSID());
- SERIAL_DEBUG_MSG("STAND IPv4: ", false);
- Serial.println(WiFi.localIP());
- Serial.print("STAND IPv6: ");
- Serial.println(WiFi.localIPv6());
- WiFi.mode(WIFI_MODE_STA);
- }
Tutaj wyświetlam komunikaty dla użytkownika oraz ustawiam wifi w tryb STA.
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:
Ładowanie strony internetowej:
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:
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.
- void wifiOnDisconnect()
- {
- SERIAL_DEBUG_MSG("STAND Disconnected", true);
- delay(500);
- WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
- }
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:
- void setHttpOrTcpServer(void)
- {
- if(wifiSSID == "")
- {
- SERIAL_DEBUG_MSG("Wifi as AP", true);
- server.begin();
- }
- else
- {
- SERIAL_DEBUG_MSG("Tcp server Enable", true);
- tcpServer.begin();
- }
- }
Ładowanie strony internetowej:
- void apHttp(void)
- {
- WiFiClient client = server.available(); // listen for incoming clients
- if (client)
- {
- SERIAL_DEBUG_MSG("New client",true);
- memset(dataBuffer,0,sizeof(dataBuffer));
- countChars=0;
- boolean currentLineIsBlank = true;
- while (client.connected())
- {
- if (client.available())
- {
- char c = client.read();
- Serial.write(c);
- dataBuffer[countChars]=c;
- if (countChars<sizeof(dataBuffer)-1) {
- countChars++;
- }
- if (c == '\n' && currentLineIsBlank)
- {
- SERIAL_DEBUG_MSG("Send response", true);
- displayWebPage(&client);
- break;
- }
- if (c == '\n')
- {
- client.println("HTTP/1.1 200 OK");
- currentLineIsBlank = true;
- if (strContain(dataBuffer, "rec_data"))
- {
- client.println("Content-Type: text/xml");
- client.println("Connection: keep-alive");
- client.println();
- if (getSSIDPasswData(ssidData, passwordData, SSID_BUFFER_SIZE))
- {
- wifiSSID = ssidData;
- wifiPassword = passwordData;
- SERIAL_DEBUG_MSG("############################", true);
- Serial.println(wifiSSID);
- Serial.println(wifiPassword);
- SERIAL_DEBUG_MSG("############################", true);
- NonVolatileStorageWrite();
- // the content of the HTTP response follows the header:
- client.print("<h1>OK! Restarting in 5 seconds...</h1>");
- client.println();
- Serial.println("Data sended, now restarting");
- delay(4000);
- ESP.restart();
- }
- }
- currentLineIsBlank = true;
- memset(dataBuffer,0,sizeof(dataBuffer));
- countChars=0;
- }
- else if (c != '\r') {
- currentLineIsBlank = false;
- }
- }
- }
- // give the web browser time to receive the data
- delay(1);
- // close the connection:
- client.stop();
- SERIAL_DEBUG_MSG("client disconnected", true);
- }
- }
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:
- void displayWebPage(WiFiClient *client)
- {
- client->println("HTTP/1.1 200 OK");
- client->println("Content-Type: text/html");
- client->println("Connection: close"); // the connection will be closed after completion of the response
- client->println();
- client->println("<!DOCTYPE HTML><html><head>");
- client->println("<body bgcolor=\"#E6E6FA\">");
- client->println("<meta charset=\"utf-8\">");
- client->println("<script>");
- client->println("ssidInfo = \"\";");
- client->println("passInfo = \"\";");
- client->println("function SendText()");
- client->println("{");
- client->println("nocache = \"&nocache=\" + Math.random() * 1000000;");
- client->println("var request = new XMLHttpRequest();");
- client->println("ssidInfo = \"&s1=\" + document.getElementById(\"txt_form\").ssidData.value;");
- client->println("passInfo = \"&p1=\" + document.getElementById(\"txt_form\").passData.value;");
- client->println("request.open(\"GET\", \"rec_data\" + ssidInfo + passInfo + nocache, true);");
- client->println("request.send(null);");
- client->println("}");
- client->println("</script>");
- client->println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></head>");
- client->println("<h1>Ustaw wifi</h1>");
- client->println("<body onload=\"WriteData\">");
- client->println("<p><b>Wprowadź dane do logowania:</b></p>");
- client->println("<form id=\"txt_form\" name=\"frmText\">");
- client->println("<label>SSID: <input type=\"text\" name=\"ssidData\" size=\"32\" maxlength=\"32\" /></label><br /><br />");
- client->println("<label>Hasło: <input type=\"text\" name=\"passData\" size=\"64\" maxlength=\"64\" /></label>");
- client->println("</form>");
- client->println("<br />");
- client->println("<input type=\"submit\" value=\"Zapisz Ustawienia\" onclick=\"SendText()\" />");
- client->println("</body>");
- client->println("</html>");
- }
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.