Hallo zusammen,
die positive Resonanz und das allgemeine Interesse zu dem Vorgängerblog über ein Captive Portal, hat mich dazu veranlasst, einen weiteren Sonderblog zu dem Thema schreiben und auf einige Punkte und Wünsche von euch im Einzelnen einzugehen. Unter vielen anderen Detailfragen, auf die ich später noch eingehe, bestand der Wunsch, das Captive Portal nicht nur auf einem ESP8266, sondern auch auf dem ESP32 laufen zu lassen. Dies ist nativ nicht möglich. Dazu müssen einige Änderungen, speziell der genutzten Librarys, vorgenommen werden. Der heutige Code ist daher NUR auf dem ESP32 lauffähig. Im Detail sind neben den Libraryanpassungen auch einige Optimierungen speziell für den ESP32 hinzugekommen.
Bei einigen Lesern des Blogs ist es darüber hinaus wohl vorgekommen, dass der ESP sich nicht mehr richtig mit einem Access Point reconnecten konnte, nachdem er rebootet wurde. Diesen Bug konnte ich in nach einer längeren Suche nachstellen. Dieser trifft wohl im Zusammenspiel mit bestimmten Access Points auf, bei der ESP trotz richtigen WLAN Zugangsdaten bei der Anmeldung an den Access Point svond diesem abgewiesen wird und folgerichtig vom Station Mode in den eigenen Access Point Mode wechselt. Bei diesem Vorgang werden die vorherigen WLAN Zugangsdaten aus dem EEPROM gelöscht (dies ist so gewollt).
Die Lösung für diesen systemseitigen Bug ist, die Anmeldung mehrere Male trotz angeblich „falschen“ Zugangsdaten zu versuchen, bevor wieder in den Access Point Mode gewechselt wird.
As weitere kleine Verbesserung wird die aktuelle IP Adresse, egal in welchem Mode sich der ESP32 befindet, auf der seriellen Schnittstelle ausgegeben. Wer also diese IP Adresse gerne auf seinem Display ausgeben möchte, muss nur noch Ausgabe an der entsprechenden Stelle im Code abändern.
Um möglichst mit vielen ESP Boards kompatibel zu sein, verzichte ich weiterhin auf die Ausgabe der Statis auf externe Ports des ESP‘s.
Auch in diesem Teil baut der ESP32 mit unserem nachfolgendem Captive Portal Code ein Captive-Portal auf. Das WLAN hat den Namen „ESP_Config“ und das Passwort „12345678“. Mit diesem können wir uns mit unserem Handy verbinden, und werden dann von dem Handy automatisch auf die Captive Portal Webseite geleitet. Diese entspricht im Design und Funktionsweise im wesentlichem dem vorherigen Teil.
Auf dieser Webseite können wir nun dem System-Link „WiFi Einstellungen“ klicken, und gelangen nun auf eine umfangreiche WLAN Konfigurationsseite, mit der wir nun sowohl ein Netz, mit dem sich der ESP32 verbinden soll, auswählen können:
Das hier ausgewählte Funknetz und das eingegebene Passwort werden im EEPROM gespeichert. Beim nächsten Boot versucht sich der ESP32 mit diesem Netzwerk zu verbinden. Scheitert dies, nach mehrmaligen Versuchen, weil das Netzwerk z.B nicht mehr erreichbar ist, oder das Passwort geändert wurde, schaltet der ESP zurück in den Access Point Modus und wartet auf eine Neukonfiguration.
Der angepasste Code für das Captive Portal für den ESP32 lautet:
[
#include <WiFi.h> #include <WiFiClient.h> #include <WebServer.h> #include <ESPmDNS.h> #include <DNSServer.h> #include <EEPROM.h> #define GPIO_OUT_W1TS_REG (DR_REG_GPIO_BASE + 0x0008) #define GPIO_OUT_W1TC_REG (DR_REG_GPIO_BASE + 0x000c) static const byte WiFiPwdLen = 25; static const byte APSTANameLen = 20; struct WiFiEEPromData { bool APSTA = true; // Access Point or Station Mode - true AP Mode bool PwDReq = false; // PasswordRequired bool CapPortal = true ; //CaptivePortal on in AP Mode char APSTAName[APSTANameLen]; // STATION /AP Point Name TO cONNECT, if definded char WiFiPwd[WiFiPwdLen]; // WiFiPAssword, if definded char ConfigValid[3]; //If Config is Vaild, Tag "TK" is required" }; /* hostname for mDNS. Should work at least on windows. Try http://esp8266.local */ const char *ESPHostname = "ESP32"; // DNS server const byte DNS_PORT = 53; DNSServer dnsServer; //Conmmon Paramenters bool SoftAccOK = false; // Web server WebServer server(80); /* Soft AP network parameters */ IPAddress apIP(172, 20, 0, 1); IPAddress netMsk(255, 255, 255, 0); unsigned long currentMillis = 0; unsigned long startMillis; /** Current WLAN status */ short status = WL_IDLE_STATUS; WiFiEEPromData MyWiFiConfig; String temp =""; void setup() { REG_WRITE(GPIO_OUT_W1TS_REG, BIT(GPIO_NUM_16)); // Guru Meditation Error Remediation set delay(1); REG_WRITE(GPIO_OUT_W1TC_REG, BIT(GPIO_NUM_16)); // Guru Meditation Error Remediation clear bool ConnectSuccess = false; bool CreateSoftAPSucc = false; bool CInitFSSystem = false; bool CInitHTTPServer = false; byte len; Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for native USB } Serial.println(F("Serial Interface initalized at 9600 Baud.")); WiFi.setAutoReconnect (false); WiFi.persistent(false); WiFi.disconnect(); WiFi.setHostname(ESPHostname); // Set the DHCP hostname assigned to ESP station. if (loadCredentials()) // Load WLAN credentials for WiFi Settings { Serial.println(F("Valid Credentials found.")); if (MyWiFiConfig.APSTA == true) // AP Mode { Serial.println(F("Access Point Mode selected.")); Serial.println(MyWiFiConfig.APSTA); len = strlen(MyWiFiConfig.APSTAName); MyWiFiConfig.APSTAName[len+1] = '\0'; len = strlen(MyWiFiConfig.WiFiPwd); MyWiFiConfig.WiFiPwd[len+1] = '\0'; CreateSoftAPSucc = CreateWifiSoftAP(); } else { Serial.println(F("Station Mode selected.")); len = strlen(MyWiFiConfig.APSTAName); MyWiFiConfig.APSTAName[len+1] = '\0'; len = strlen(MyWiFiConfig.WiFiPwd); MyWiFiConfig.WiFiPwd[len+1] = '\0'; len = ConnectWifiAP(); if ( len == 3 ) { ConnectSuccess = true; } else { ConnectSuccess = false; } } } else { //Set default Config - Create AP Serial.println(F("NO Valid Credentials found.")); SetDefaultWiFiConfig (); CreateSoftAPSucc = CreateWifiSoftAP(); saveCredentials(); delay(500); } if ((ConnectSuccess or CreateSoftAPSucc)) { Serial.print (F("IP Address: ")); if (CreateSoftAPSucc) { Serial.println(WiFi.softAPIP());} if (ConnectSuccess) { Serial.println(WiFi.localIP());} InitalizeHTTPServer(); } else { Serial.setDebugOutput(true); //Debug Output for WLAN on Serial Interface. Serial.println(F("Error: Cannot connect to WLAN. Set DEFAULT Configuration.")); SetDefaultWiFiConfig(); CreateSoftAPSucc = CreateWifiSoftAP(); InitalizeHTTPServer(); SetDefaultWiFiConfig(); saveCredentials(); } } void InitalizeHTTPServer() { bool initok = false; /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */ server.on("/", handleRoot); server.on("/wifi", handleWifi); if (MyWiFiConfig.CapPortal) { server.on("/generate_204", handleRoot); } //Android captive portal. Maybe not needed. Might be handled by notFound handler. if (MyWiFiConfig.CapPortal) { server.on("/favicon.ico", handleRoot); } //Another Android captive portal. Maybe not needed. Might be handled by notFound handler. Checked on Sony Handy if (MyWiFiConfig.CapPortal) { server.on("/fwlink", handleRoot); } //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler. //server.on("/generate_204", handleRoot); //Android captive portal. Maybe not needed. Might be handled by notFound handler. //server.on("/favicon.ico", handleRoot); //Another Android captive portal. Maybe not needed. Might be handled by notFound handler. Checked on Sony Handy //server.on("/fwlink", handleRoot); //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler. server.onNotFound ( handleNotFound ); // Speicherung Header-Elemente anfordern // server.collectHeaders(Headers, sizeof(Headers)/ sizeof(Headers[0])); server.begin(); // Web server start } boolean CreateWifiSoftAP() { WiFi.disconnect(); Serial.print(F("Initalize SoftAP ")); if (MyWiFiConfig.PwDReq) { SoftAccOK = WiFi.softAP(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd); // Passwortlänge mindestens 8 Zeichen ! } else { SoftAccOK = WiFi.softAP(MyWiFiConfig.APSTAName); // Access Point WITHOUT Password // Overload Function:; WiFi.softAP(ssid, password, channel, hidden) } delay(2000); // Without delay I've seen the IP address blank WiFi.softAPConfig(apIP, apIP, netMsk); if (SoftAccOK) { /* Setup the DNS server redirecting all the domains to the apIP */ dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(DNS_PORT, "*", apIP); Serial.println(F("successful.")); // Serial.setDebugOutput(true); // Debug Output for WLAN on Serial Interface. } else { Serial.println(F("Soft AP Error.")); Serial.println(MyWiFiConfig.APSTAName); Serial.println(MyWiFiConfig.WiFiPwd); } return SoftAccOK; } byte ConnectWifiAP() { Serial.println(F("Initalizing Wifi Client.")); byte connRes = 0; byte i = 0; WiFi.disconnect(); WiFi.softAPdisconnect(true); // Function will set currently configured SSID and password of the soft-AP to null values. The parameter is optional. If set to true it will switch the soft-AP mode off. delay(500); WiFi.begin(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd); connRes = WiFi.waitForConnectResult(); while (( connRes == 0 ) and (i != 10)) //if connRes == 0 "IDLE_STATUS - change Statius" { connRes = WiFi.waitForConnectResult(); delay(2000); i++; Serial.print(F(".")); // statement(s) } while (( connRes == 1 ) and (i != 10)) //if connRes == 1 NO_SSID_AVAILin - SSID cannot be reached { connRes = WiFi.waitForConnectResult(); delay(2000); i++; Serial.print(F(".")); // statement(s) } if (connRes == 3 ) { WiFi.setAutoReconnect(true); // Set whether module will attempt to reconnect to an access point in case it is disconnected. // Setup MDNS responder if (!MDNS.begin(ESPHostname)) { Serial.println(F("Error: MDNS")); } else { MDNS.addService("http", "tcp", 80); } } while (( connRes == 4 ) and (i != 10)) //if connRes == 4 Bad Password. Sometimes happens this with corrct PWD { WiFi.begin(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd); connRes = WiFi.waitForConnectResult(); delay(3000); i++; Serial.print(F(".")); } if (connRes == 4 ) { Serial.println(F("STA Pwd Err")); Serial.println(MyWiFiConfig.APSTAName); Serial.println(MyWiFiConfig.WiFiPwd); WiFi.disconnect(); } // if (connRes == 6 ) { Serial.println("DISCONNECTED - Not in station mode"); } // WiFi.printDiag(Serial); Serial.println(""); return connRes; } #define SD_BUFFER_PIXELS 20 /** Load WLAN credentials from EEPROM */ bool loadCredentials() { bool RetValue; EEPROM.begin(512); EEPROM.get(0, MyWiFiConfig); EEPROM.end(); if (String(MyWiFiConfig.ConfigValid) = String("TK")) { RetValue = true; } else { RetValue = false; // WLAN Settings not found. } return RetValue; } /** Store WLAN credentials to EEPROM */ bool saveCredentials() { bool RetValue; // Check logical Errors RetValue = true; if (MyWiFiConfig.APSTA == true ) //AP Mode { if (MyWiFiConfig.PwDReq and (sizeof(String(MyWiFiConfig.WiFiPwd)) < 8)) { RetValue = false; // Invalid Config } if (sizeof(String(MyWiFiConfig.APSTAName)) < 1) { RetValue = false; // Invalid Config } } if (RetValue) { EEPROM.begin(512); for (int i = 0 ; i < sizeof(MyWiFiConfig) ; i++) { EEPROM.write(i, 0); } strncpy( MyWiFiConfig.ConfigValid , "TK", sizeof(MyWiFiConfig.ConfigValid) ); EEPROM.put(0, MyWiFiConfig); EEPROM.commit(); EEPROM.end(); } return RetValue; } void SetDefaultWiFiConfig() { byte len; MyWiFiConfig.APSTA = true; MyWiFiConfig.PwDReq = true; // default PW required MyWiFiConfig.CapPortal = true; strncpy( MyWiFiConfig.APSTAName, "ESP_Config", sizeof(MyWiFiConfig.APSTAName) ); len = strlen(MyWiFiConfig.APSTAName); MyWiFiConfig.APSTAName[len+1] = '\0'; strncpy( MyWiFiConfig.WiFiPwd, "12345678", sizeof(MyWiFiConfig.WiFiPwd) ); len = strlen(MyWiFiConfig.WiFiPwd); MyWiFiConfig.WiFiPwd[len+1] = '\0'; strncpy( MyWiFiConfig.ConfigValid, "TK", sizeof(MyWiFiConfig.ConfigValid) ); len = strlen(MyWiFiConfig.ConfigValid); MyWiFiConfig.ConfigValid[len+1] = '\0'; Serial.println(F("Reset WiFi Credentials.")); } void handleRoot() { // Main Page: temp = ""; byte PicCount = 0; byte ServArgs = 0; // HTML Header server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); server.setContentLength(CONTENT_LENGTH_UNKNOWN); // HTML Content server.send ( 200, "text/html", temp ); // Speichersparen - Schon mal dem Cleint senden temp = ""; temp += "<!DOCTYPE HTML><html lang='de'><head><meta charset='UTF-8'><meta name= viewport content='width=device-width, initial-scale=1.0,'>"; server.sendContent(temp); temp = ""; temp += "<style type='text/css'><!-- DIV.container { min-height: 10em; display: table-cell; vertical-align: middle }.button {height:35px; width:90px; font-size:16px}"; server.sendContent(temp); temp = ""; temp += "body {background-color: powderblue;}</style>"; temp += "<head><title>Hauptseite</title></head>"; temp += "<h2>Hauptseite</h2>"; temp += "<body>"; server.sendContent(temp); temp = ""; // Processing User Request temp = ""; temp += "<table border=2 bgcolor = white width = 500 cellpadding =5 ><caption><p><h3>Systemlinks:</h2></p></caption>"; temp += "<tr><th><br>"; temp += "<a href='/wifi'>WIFI Einstellungen</a><br><br>"; temp += "</th></tr></table><br><br>"; temp += "<footer><p>Programmed and designed by: Tobias Kuch</p><p>Contact information: <a href='mailto:tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com</a>.</p></footer>"; temp += "</body></html>"; server.sendContent(temp); temp = ""; server.client().stop(); // Stop is needed because we sent no content length } void handleNotFound() { if (captivePortal()) { // If caprive portal redirect instead of displaying the error page. return; } temp = ""; // HTML Header server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); server.setContentLength(CONTENT_LENGTH_UNKNOWN); // HTML Content temp += "<!DOCTYPE HTML><html lang='de'><head><meta charset='UTF-8'><meta name= viewport content='width=device-width, initial-scale=1.0,'>"; temp += "<style type='text/css'><!-- DIV.container { min-height: 10em; display: table-cell; vertical-align: middle }.button {height:35px; width:90px; font-size:16px}"; temp += "body {background-color: powderblue;}</style>"; temp += "<head><title>File not found</title></head>"; temp += "<h2> 404 File Not Found</h2><br>"; temp += "<h4>Debug Information:</h4><br>"; temp += "<body>"; temp += "URI: "; temp += server.uri(); temp += "\nMethod: "; temp+= ( server.method() == HTTP_GET ) ? "GET" : "POST"; temp += "<br>Arguments: "; temp += server.args(); temp += "\n"; for ( uint8_t i = 0; i < server.args(); i++ ) { temp += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n"; } temp += "<br>Server Hostheader: "+ server.hostHeader(); for ( uint8_t i = 0; i < server.headers(); i++ ) { temp += " " + server.headerName ( i ) + ": " + server.header ( i ) + "\n<br>"; } temp += "</table></form><br><br><table border=2 bgcolor = white width = 500 cellpadding =5 ><caption><p><h2>You may want to browse to:</h2></p></caption>"; temp += "<tr><th>"; temp += "<a href='/'>Main Page</a><br>"; temp += "<a href='/wifi'>WIFI Settings</a><br>"; temp += "</th></tr></table><br><br>"; temp += "<footer><p>Programmed by: Tobias Kuch</p><p>Contact information: <a href='mailto:tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com</a>.</p></footer>"; temp += "</body></html>"; server.send ( 404, "", temp ); server.client().stop(); // Stop is needed because we sent no content length temp = ""; } /** Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */ boolean captivePortal() { if (!isIp(server.hostHeader()) && server.hostHeader() != (String(ESPHostname)+".local")) { // Serial.println("Request redirected to captive portal"); server.sendHeader("Location", String("http://") + toStringIp(server.client().localIP()), true); server.send ( 302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. server.client().stop(); // Stop is needed because we sent no content length return true; } return false; } /** Wifi config page handler */ void handleWifi() { // Page: /wifi byte i; byte len ; temp = ""; // Check for Site Parameters if (server.hasArg("Reboot") ) // Reboot System { temp = "Rebooting System in 5 Seconds.."; server.send ( 200, "text/html", temp ); delay(5000); server.client().stop(); WiFi.disconnect(); delay(1000); } if (server.hasArg("WiFiMode") and (server.arg("WiFiMode") == "1") ) // STA Station Mode Connect to another WIFI Station { startMillis = millis(); // Reset Time Up Counter to avoid Idle Mode whiole operating // Connect to existing STATION if ( sizeof(server.arg("WiFi_Network")) > 0 ) { Serial.println("STA Mode"); MyWiFiConfig.APSTA = false; // Access Point or Station Mode - false Station Mode temp = ""; for ( i = 0; i < APSTANameLen;i++) { MyWiFiConfig.APSTAName[i] = 0; } temp = server.arg("WiFi_Network"); len = temp.length(); for ( i = 0; i < len;i++) { MyWiFiConfig.APSTAName[i] = temp[i]; } temp = ""; for ( i = 0; i < WiFiPwdLen;i++) { MyWiFiConfig.WiFiPwd[i] = 0; } temp = server.arg("STAWLanPW"); len = temp.length(); for ( i = 0; i < len;i++) { if (temp[i] > 32) //Steuerzeichen raus { MyWiFiConfig.WiFiPwd[i] = temp[i]; } } temp = "WiFi Connect to AP: -"; temp += MyWiFiConfig.APSTAName; temp += "-<br>WiFi PW: -"; temp += MyWiFiConfig.WiFiPwd; temp += "-<br>"; temp += "Connecting to STA Mode in 2 Seconds..<br>"; server.send ( 200, "text/html", temp ); server.sendContent(temp); delay(2000); server.client().stop(); server.stop(); temp = ""; WiFi.disconnect(); WiFi.softAPdisconnect(true); delay(500); // ConnectWifiAP bool SaveOk = saveCredentials(); i = ConnectWifiAP(); delay(700); if (i != 3) // 4: WL_CONNECT_FAILED - Password is incorrect 1: WL_NO_SSID_AVAILin - Configured SSID cannot be reached { Serial.print(F("Cannot Connect to specified Network. Reason: ")); Serial.println(i); server.client().stop(); delay(100); WiFi.setAutoReconnect (false); delay(100); WiFi.disconnect(); delay(1000); SetDefaultWiFiConfig(); CreateWifiSoftAP(); return; } else { // Safe Config bool SaveOk = saveCredentials(); InitalizeHTTPServer(); return; } } } if (server.hasArg("WiFiMode") and (server.arg("WiFiMode") == "2") ) // Change AP Mode { startMillis = millis(); // Reset Time Up Counter to avoid Idle Mode whiole operating // Configure Access Point temp = server.arg("APPointName"); len = temp.length(); temp =server.arg("APPW"); if (server.hasArg("PasswordReq")) { i = temp.length(); } else { i = 8; } if ( ( len > 1 ) and (server.arg("APPW") == server.arg("APPWRepeat")) and ( i > 7) ) { temp = ""; Serial.println(F("APMode")); MyWiFiConfig.APSTA = true; // Access Point or Sation Mode - true AP Mode if (server.hasArg("CaptivePortal")) { MyWiFiConfig.CapPortal = true ; //CaptivePortal on in AP Mode } else { MyWiFiConfig.CapPortal = false ; } if (server.hasArg("PasswordReq")) { MyWiFiConfig.PwDReq = true ; //Password Required in AP Mode } else { MyWiFiConfig.PwDReq = false ; } for ( i = 0; i < APSTANameLen;i++) { MyWiFiConfig.APSTAName[i] = 0; } temp = server.arg("APPointName"); len = temp.length(); for ( i = 0; i < len;i++) { MyWiFiConfig.APSTAName[i] = temp[i]; } MyWiFiConfig.APSTAName[len+1] = '\0'; temp = ""; for ( i = 0; i < WiFiPwdLen;i++) { MyWiFiConfig.WiFiPwd[i] = 0; } temp = server.arg("APPW"); len = temp.length(); for ( i = 0; i < len;i++) { MyWiFiConfig.WiFiPwd[i] = temp[i]; } MyWiFiConfig.WiFiPwd[len+1] = '\0'; temp = ""; if (saveCredentials()) // Save AP ConfigCongfig { temp = "Daten des AP Modes erfolgreich gespeichert. Reboot notwendig."; } else { temp = "Daten des AP Modes fehlerhaft."; } } else if (server.arg("APPW") != server.arg("APPWRepeat")) { temp = ""; temp = "WLAN Passwort nicht gleich. Abgebrochen."; } else { temp = ""; temp = "WLAN Passwort oder AP Name zu kurz. Abgebrochen."; } } // HTML Header server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); server.setContentLength(CONTENT_LENGTH_UNKNOWN); // HTML Content temp += "<!DOCTYPE HTML><html lang='de'><head><meta charset='UTF-8'><meta name= viewport content='width=device-width, initial-scale=1.0,'>"; server.send ( 200, "text/html", temp ); temp = ""; temp += "<style type='text/css'><!-- DIV.container { min-height: 10em; display: table-cell; vertical-align: middle }.button {height:35px; width:90px; font-size:16px}"; temp += "body {background-color: powderblue;}</style><head><title>Smartes Tuerschild - WiFi Settings</title></head>"; server.sendContent(temp); temp = ""; temp += "<h2>WiFi Einstellungen</h2><body><left>"; temp += "<table border=2 bgcolor = white width = 500 ><td><h4>Current WiFi Settings: </h4>"; if (server.client().localIP() == apIP) { temp += "Mode : Soft Access Point (AP)<br>"; temp += "SSID : " + String (MyWiFiConfig.APSTAName) + "<br><br>"; } else { temp += "Mode : Station (STA) <br>"; temp += "SSID : "+ String (MyWiFiConfig.APSTAName) + "<br>"; temp += "BSSID : " + WiFi.BSSIDstr()+ "<br><br>"; } temp += "</td></table><br>"; server.sendContent(temp); temp = ""; temp += "<form action='/wifi' method='post'>"; temp += "<table border=2 bgcolor = white width = 500><tr><th><br>"; if (MyWiFiConfig.APSTA == 1) { temp += "<input type='radio' value='1' name='WiFiMode' > WiFi Station Mode<br>"; } else { temp += "<input type='radio' value='1' name='WiFiMode' checked > WiFi Station Mode<br>"; } temp += "Available WiFi Networks:<table border=2 bgcolor = white ></tr></th><td>Number </td><td>SSID </td><td>Encryption </td><td>WiFi Strength </td>"; server.sendContent(temp); temp = ""; WiFi.scanDelete(); int n = WiFi.scanNetworks(false, false); //WiFi.scanNetworks(async, show_hidden) if (n > 0) { for (int i = 0; i < n; i++) { temp += "</tr></th>"; String Nrb = String(i); temp += "<td>" + Nrb + "</td>"; temp += "<td>" + WiFi.SSID(i) +"</td>"; Nrb = GetEncryptionType(WiFi.encryptionType(i)); temp += "<td>"+ Nrb + "</td>"; temp += "<td>" + String(WiFi.RSSI(i)) + "</td>"; } } else { temp += "</tr></th>"; temp += "<td>1 </td>"; temp += "<td>No WLAN found</td>"; temp += "<td> --- </td>"; temp += "<td> --- </td>"; } temp += "</table><table border=2 bgcolor = white ></tr></th><td>Connect to WiFi SSID: </td><td><select name='WiFi_Network' >"; if (n > 0) { for (int i = 0; i < n; i++) { temp += "<option value='" + WiFi.SSID(i) +"'>" + WiFi.SSID(i) +"</option>"; } } else { temp += "<option value='No_WiFi_Network'>No WiFiNetwork found !/option>"; } server.sendContent(temp); temp = ""; temp += "</select></td></tr></th></tr></th><td>WiFi Password: </td><td>"; temp += "<input type='text' name='STAWLanPW' maxlength='40' size='40'>"; temp += "</td></tr></th><br></th></tr></table></table><table border=2 bgcolor = white width = 500 ><tr><th><br>"; server.sendContent(temp); temp = ""; if (MyWiFiConfig.APSTA == true) { temp += "<input type='radio' name='WiFiMode' value='2' checked> WiFi Access Point Mode <br>"; } else { temp += "<input type='radio' name='WiFiMode' value='2' > WiFi Access Point Mode <br>"; } temp += "<table border=2 bgcolor = white ></tr></th> <td>WiFi Access Point Name: </td><td>"; server.sendContent(temp); temp = ""; if (MyWiFiConfig.APSTA == true) { temp += "<input type='text' name='APPointName' maxlength='"+String(APSTANameLen-1)+"' size='30' value='" + String(MyWiFiConfig.APSTAName) + "'></td>"; } else { temp += "<input type='text' name='APPointName' maxlength='"+String(APSTANameLen-1)+"' size='30' ></td>"; } server.sendContent(temp); temp = ""; if (MyWiFiConfig.APSTA == true) { temp += "</tr></th><td>WiFi Password: </td><td>"; temp += "<input type='password' name='APPW' maxlength='"+String(WiFiPwdLen-1)+"' size='30' value='" + String(MyWiFiConfig.WiFiPwd) + "'> </td>"; temp += "</tr></th><td>Repeat WiFi Password: </td>"; temp += "<td><input type='password' name='APPWRepeat' maxlength='"+String(WiFiPwdLen-1)+"' size='30' value='" + String(MyWiFiConfig.WiFiPwd) + "'> </td>"; } else { temp += "</tr></th><td>WiFi Password: </td><td>"; temp += "<input type='password' name='APPW' maxlength='"+String(WiFiPwdLen-1)+"' size='30'> </td>"; temp += "</tr></th><td>Repeat WiFi Password: </td>"; temp += "<td><input type='password' name='APPWRepeat' maxlength='"+String(WiFiPwdLen-1)+"' size='30'> </td>"; } temp += "</table>"; server.sendContent(temp); temp = ""; if (MyWiFiConfig.PwDReq) { temp += "<input type='checkbox' name='PasswordReq' checked> Password for Login required. "; } else { temp += "<input type='checkbox' name='PasswordReq' > Password for Login required. "; } server.sendContent(temp); temp = ""; if (MyWiFiConfig.CapPortal) { temp += "<input type='checkbox' name='CaptivePortal' checked> Activate Captive Portal"; } else { temp += "<input type='checkbox' name='CaptivePortal' > Activate Captive Portal"; } server.sendContent(temp); temp = ""; temp += "<br></tr></th></table><br> <button type='submit' name='Settings' value='1' style='height: 50px; width: 140px' autofocus>Set WiFi Settings</button>"; temp += "<button type='submit' name='Reboot' value='1' style='height: 50px; width: 200px' >Reboot System</button>"; server.sendContent(temp); temp = ""; temp += "<button type='reset' name='action' value='1' style='height: 50px; width: 100px' >Reset</button></form>"; temp += "<table border=2 bgcolor = white width = 500 cellpadding =5 ><caption><p><h3>Systemlinks:</h2></p></caption><tr><th><br>"; server.sendContent(temp); temp = ""; temp += "<a href='/'>Main Page</a><br><br></th></tr></table><br><br>"; temp += "<footer><p>Programmed and designed by: Tobias Kuch</p><p>Contact information: <a href='mailto:tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com</a>.</p></footer>"; temp += "</body></html>"; server.sendContent(temp); server.client().stop(); // Stop is needed because we sent no content length temp = ""; } /** Is this an IP? */ boolean isIp(String str) { for (int i = 0; i < str.length(); i++) { int c = str.charAt(i); if (c != '.' && (c < '0' || c > '9')) { return false; } } return true; } String GetEncryptionType(byte thisType) { String Output = ""; // read the encryption type and print out the name: switch (thisType) { case 5: Output = "WEP"; return Output; break; case 2: Output = "WPA"; return Output; break; case 4: Output = "WPA2"; return Output; break; case 7: Output = "None"; return Output; break; case 8: Output = "Auto"; return Output; break; } } /** IP to String? */ String toStringIp(IPAddress ip) { String res = ""; for (int i = 0; i < 3; i++) { res += String((ip >> (8 * i)) & 0xFF) + "."; } res += String(((ip >> 8 * 3)) & 0xFF); return res; } String formatBytes(size_t bytes) { // lesbare Anzeige der Speichergrößen if (bytes < 1024) { return String(bytes) + " Byte"; } else if (bytes < (1024 * 1024)) { return String(bytes / 1024.0) + " KB"; } else if (bytes < (1024 * 1024 * 1024)) { return String(bytes / 1024.0 / 1024.0) + " MB"; } } void loop() { if (SoftAccOK) { dnsServer.processNextRequest(); //DNS } //HTTP server.handleClient(); }]
Im nächsten Teil wollen wir uns einmal die praktische Verwendung unseres Codes anschauen, und bauen uns, basierend auf diesem Code einen kleinen Fileserver auf.
Ich wünsche viel Spaß mit dem Captive Portal und bei der Implementierung in eigene ESP32 Projekte.
18 comments
Walter
Hi, this code is bugged, please fix it. Thanks,
Jan
Hallo Gerald, herzlichsten Dank!!! Ich hatte da schon einige Stunden herumprobiert, aber immer die falsche Stelle erwischt. Es ist exakt so wie beschrieben: durch das Auskommentieren dieser Zeile in der setup() werden die Credentials nicht überschrieben, dennoch wird ein AP angelegt mit den hinterlegten Zugangsdaten. Dadurch ist es egal, wenn das Gerät mal nicht ins WLAN kommen sollte, der AP stört ja nicht und ließe sich auch durch einen Button aktivieren. Danke nochmal!
Gerald Lechner
Hallo Jan
Ich würde in der Setup-Routine
if ((ConnectSuccess or CreateSoftAPSucc))
{
Serial.print (F("IP Address: “));
if (CreateSoftAPSucc) { Serial.println(WiFi.softAPIP());}
if (ConnectSuccess) { Serial.println(WiFi.localIP());}
InitalizeHTTPServer();
}
else
{
Serial.setDebugOutput(true); //Debug Output for WLAN on Serial Interface.
Serial.println(F(”Error: Cannot connect to WLAN. Set DEFAULT Configuration."));
SetDefaultWiFiConfig();
CreateSoftAPSucc = CreateWifiSoftAP();
InitalizeHTTPServer();
SetDefaultWiFiConfig();
saveCredentials();
}
}
die Zeile saveCredentials(); auskommentieren. Die wird nur dann aufgerufen, wenn keine Verbindung möglich war. Wenn die Credentials hier nicht gespeichert werden, geht der ESP32 zwar in den AP-Modus, speichert diesen Zustand aber nicht ab. Damit werden beim nächsten Start wieder die abgespeicherten Credentials für den Station-Mode genutzt.
Jan
Herzlichen Dank für den tollen Sketch, läuft mit den kleinen Korrekturen soweit sehr gut. Leider werden die manuell gespeicherten WLAN-Daten gelöscht, sobald das WLAN beim Start nicht verfügbar ist. Offenbar werden die Zugangsdaten für AP und STA an der gleichen Stelle im eeprom gespeichert, sodass die vorigen Zugangsdaten unwiederbringlich gelöscht werden, sobald das Gerät in den AP-Modus wechselt. Frage: wie ließen sich die AP-Zugangdaten hart festschreiben, um zu verhindern, dass die eingetragenen WLAN-Daten im AP-Modus gelöscht werden? Im eeprom möchte ich ausschließlich die Daten für den STA-Modus speichern.
Jan
Ein sehr schöner Sketch, herzlichen Dank für die Arbeit und Veröffentlichung. Mit den Tipps aus den Kommentaren läuft das soweit sehr zuverlässig auf meinem ESP32.
Leider stört ein ‘Feature’ in meinem Projekt ganz erheblich: Findet der ESP beim Starten das vorher konfigurierte WLAN nicht, werden die Zugangsdaten komplett gelöscht. Das ist bei temporärem oder instalbilem WLAN ziemlich störend. Meine Frage in die Runde: an welcher Stelle kann ich das am besten verhindern? Die Routine für den Reset-Knopf möchte ich ja nicht komplett löschen. Ziel: WLAN-Zugangsdaten werden nur durch Reset oder Überschreiben gelöscht, nicht aber automatisch.
Danke für Tipps!!!
Achim
Hi kann mir einer schreiben wie diese ESPmDNS.h finde und einbinde .
Normal kann ich Bibilotheken einbinden aber diese finde ich nicht oder die IDE meckert das die Zip keine Bibliothek enthält
Tobias
Sehr geehrter Herr Holler,
Ich muss leider 2 Punkte korrigieren. Zum einen ist die Beschränkung auf 20 Zeichen der SSID kein Fehler sondern eine Limitierung, die im Sinne des open Source Gedankens angepasst werden kann. Zum anderen werden Tests der Software beim Entwickeln und kursorische Abschlusstests vor Veröffentlichung durchgeführt. Ihre generalisierte Aussage, das Tests überhaupt nicht durchgeführt werden ist daher nicht korrekt.
Jedoch sind generell leider auch zeitliche Limitierungen bei der Entwicklung einzuhalten, sodass eben Bugs nicht ganz vermieden werden können. Ich freue mich jedoch, das diese sich bisher auf ein Bug bei einem komplexem Code wie diesen beschränken. Sachliche Hinweise auf diese oder für alle relevante Verbesserungswünsche, ohne Wertung, sind daher immer willkommen und werden im nächsten Release ggf. berücksichtigt.
Walter Holler
Wäre es zu viel verlangt, den veröffentlichten Code vorher mal selbst auszuprobieren?
Ich habe den Code auf meinen neu gekauften ESP32 geladen und mindestens 2 Fehler gefunden:
1) Der Vergleichs-Operator für Gleichheit ist “==”. Das einfache “=” ist eine Zuweisung.
Also in loadCredentials() :
if (String(MyWiFiConfig.ConfigValid) == String(“TK”))
statt
if (String(MyWiFiConfig.ConfigValid) = String(“TK”))
2) Die maximale Länge der SSID Namen ist 32 nicht 20, sonst funktioniert es nicht bei längeren Namen wie bei mir.
static const byte APSTANameLen = 32; //was 20
Dann funktioniert auch der AP Mode, Auswahl des WLANs und Eingabe des Passwords und Wechsel in mein WLAN.
Jedoch nach Reset des ESP32 startet er wieder im AP mode..?
MCL
|ich kann kein ESP_Config finden… Nur ein ESP_C9013b….
das liegt auch an ‘alten’ credentials.
‘C9013b’ ist der zweite Teil der MAC Adresse…
Man kann die Kommandos zum Überschreiben der Credantials ‘zu Fuss’ schreiben oder so:
// if (false) // overwrite only ONCE!!
if (loadCredentials()) // Load WLAN credentials for WiFi Settings
Also einmals mit ’ if (false) ’ kompilieren und laden.
=> so werden die Credentials garantiert korrekt überschrieben
Danach wieder auskommentieren und statt dessen ’ if (loadCredentials()) ’ verwenden und laden.
Manfred
Einfach den Code in einen vernünftigen Editor (z.B. Notpad++) kopieren und schon siehst Du, dass diese Klammer das Gegenstück von Zeile 1 ist. Fehlersuche dauerte nicht länger als die Eingabe Deiner Kommentars.
Tobias
Hallo Matthias,
Gleiches Problem und gleiche wie bei Christoph.
Gruß
Tobias
Hallo Christoph,
Dies liegt darin begründet, das der ESP intern die Credentials speichert, (von deinem vorherigen Projekt) . Um diese loszuwerden, führe EINMALIG in der Setup Routine folgende Sequenz aus:
SetDefaultWiFiConfig();
saveCredentials();
CreateSoftAPSucc = CreateWifiSoftAP();
Damit werden die Daten von dem Vorprojekt überschrieben. Nachdem diese überschrieben wurden, können die Zeilen wieder entfernt werden.
Wolfgang
Gibt es eigentlich auch einen Teil 1?
Ulrich Klaas
Hallo,
im Code für den 8266 ist die Fehlerbehebung aber noch nicht drin ??
Der hat sich nicht verändert. Geschieht das noch ?
mfG
Ulli
Peter Pitzeier
In bool loadCredentials() muss natürlich
if (String(MyWiFiConfig.ConfigValid) == String(“TK”))
und nicht
if (String(MyWiFiConfig.ConfigValid) = String(“TK”))
Christoph O
Danke für das CapturePortal-Besipiel!
Ich habe bei mir das Problem, dass ich vor einiger Zeit bereits einmal einen anderen WLAN-Accesspoint geflasht habe. Die SSID habe ich damals “Heinzelmännchen” genannt.
Nun erscheint nach dem flashen dieses Beispiels wieder der Name “Heinzelmännchen” als SSID und nicht der angegebene “ESP_Config”. Ich vermute, dass “Heinzelmännchen” noch aus alten Zeiten im EEPROM steht und nicht überschrieben wurde.
Ich kann mich leider nicht mit dem Passwort “12345678” verbinden. Das alte Passwort von Heinzelmännchen kenne ich leider nicht mehr.
Kann mir irgendwer weiterhelfen?
Klar, ich könnte wieder ein eigenen WLAN-Accesspoint flashen und alles überschreiben aber hier scheint trotzdem noch ein Bug im Quelltext zu sein.
Matthias
Hallo Zusammen,
ich kann kein ESP_Config finden… Nur ein ESP_C9013b….
Das Passwort funktioniert bei dem letzteren auch nicht.
Ich habe nichts an dem Arduino scetch verändert.
Hat sonst jemand noch dieses Problem?
Ich benutze ein Lolin D32 Pro mit einem ESP32 Wrover Chip
Danke und Gruss
Matthias
Joe
Die
]
gehört wo hin ????