Captive Portal für den ESP32 Teil 2 - AZ-Delivery

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, "