2

I have an ESP8266 project that requires setting the SSID and password for a network via an Access Point. Everything works fine, the only issue is if you send incorrect credentials to the access point, the server returns an empty response before the timeout period elapses, and the error message is never sent to the client.

If the connection is successful then the success message is appropriately sent to the client. It's almost like the WiFi.begin() function detects the connection fails on its own, and something there is preventing me from sending my own response.

%  curl -X POST "http://192.168.1.1/authenticate?ssid=FakeNetworkpassword=87654321" 
curl: (52) Empty reply from server

Did some research and saw some mentions to keep the loop running, so I attempted to do so below, but to no avail. Would appreciate any ideas, thanks.

#include "ESP8266WiFi.h"
#include "ESP8266HTTPClient.h"
#include "ESP8266WebServer.h"

HTTPClient http;
WiFiClient client;

ESP8266WebServer server(80);
IPAddress ipAP(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

unsigned long previousMillisConnection = 0;
unsigned long startingMillisConnection = 0;
const unsigned long CONNECTION_PERIOD = 1000;
const unsigned long CONNECTION_TIMEOUT = 10000;

int DELAY = 40;

char ssid[64] = { 0 };
char password[64] = { 0 };

char ssidNew[64];
char passNew[64];

const int STATE_DEFAULT = 0;
const int STATE_CREDS = 1;
int state = STATE_DEFAULT;

void setup() {
  Serial.begin(9600);
  
  WiFi.softAPConfig(ipAP, gateway, subnet);
  WiFi.softAP("ESP8266", "12345678");
  delay(100);
  
  server.on("/authenticate", HTTP_POST, onSetCredentials);
  server.begin();
  
  performWifiConnect(ssid, password);
  WiFi.waitForConnectResult(CONNECTION_TIMEOUT);
  
  Serial.println("Initialization complete");
}

void loop() {
  if (state == STATE_DEFAULT) {
    server.handleClient();
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected!");
    }
  }

  else if (state == STATE_CREDS) {
    whileConnecting();
  }
  
  delay(DELAY);
}

void performWifiConnect(char ssid[], char password[]) {
  Serial.println("Attempting to connect to " + String(ssid) + "...");
  WiFi.setAutoReconnect(false);
  WiFi.persistent(false);
  WiFi.begin(ssid, password);
}

void onWifiConnected() {
  Serial.println("WiFi connected " + WiFi.localIP().toString());
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
}

void onSetCredentials() {
  Serial.println("onSetCredentials");
  
  server.arg("ssid").toCharArray(ssidNew, 64);
  server.arg("password").toCharArray(passNew, 64);

  if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
    Serial.println("Already connected to " + String(ssidNew));
    server.send(200, "application/json", "{ \"success\": true, \"changed\": false }");
  } else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    state = STATE_CREDS;
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
  }
}

void whileConnecting() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillisConnection >= CONNECTION_PERIOD) {
    previousMillisConnection = currentMillis;

    Serial.println("checking connection...");
    
    // Success
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected to " + String(ssidNew) + " successfully!");
      
      strcpy(ssid, ssidNew);
      strcpy(password, passNew);
      
      server.send(200, "application/json", "{ \"success\": true, \"changed\": true }");

      state = STATE_DEFAULT;
    }
    
    // Failure
    if (currentMillis - startingMillisConnection >= CONNECTION_TIMEOUT) {
      Serial.println("connection failed, attempting to reconnect to old network!");
      Serial.println("ssidOld: " + String(ssid));
      Serial.println("passOld: " + String(password));

      String errorMessage = "Timed out while attempting to connect to " + String(ssidNew);
      Serial.println(errorMessage);
      server.send(500, "application/json", "{\"errorMessage\":\"" + errorMessage + "\"}");
      
      performWifiConnect(ssid, password);
      
      WiFi.waitForConnectResult(CONNECTION_TIMEOUT);

      state = STATE_DEFAULT;
      
      if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Could not return to old network!");
      }
    }
  }
}

2 Answers 2

1
+50

I recommend you to use the WiFiManager library. It has the WiFi provisioning solved.


To join an AP esp8266 must change the WiFi radio channel to the channel of the AP. Since the esp8266 has only one radio, the SoftAP must change the channel too. This causes the SoftAP clients to disconnect.

The solution is respond to the configuration request before connecting to AP, then let the configuration page do a new request for the result.

To illustrate here is a simple example:

#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif

void setup() {

  Serial.begin(115200);
  delay(500);

#ifdef ESP32
//  WiFi.begin(); // use SSID and password stored by SDK. commented out to test the Configuration AP
#else
//  WiFi.disconnect(); // forget the persistent connection to test the Configuration AP
#endif

  // waiting for connection to remembered  Wifi network
  Serial.println("Waiting for connection to WiFi");
  WiFi.waitForConnectResult();

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println();
    Serial.println("Could not connect to WiFi. Starting configuration AP...");
    configAP();
  } else {
    Serial.println("WiFi connected");
  }
}

void loop() {
}

void configAP() {

  WiFiServer configWebServer(80);

  WiFi.mode(WIFI_AP_STA); // starts the default AP (factory default or setup as persistent)

  Serial.print("Connect your computer to the WiFi network ");
#ifdef ESP32
  Serial.print("to SSID of you ESP32"); // no getter for SoftAP SSID
#else
  Serial.print(WiFi.softAPSSID());
#endif
  Serial.println();
  IPAddress ip = WiFi.softAPIP();
  Serial.print("and enter http://");
  Serial.print(ip);
  Serial.println(" in a Web browser");

  configWebServer.begin();

  while (true) {

    WiFiClient client = configWebServer.available();
    if (client) {
      char line[64];
      int l = client.readBytesUntil('\n', line, sizeof(line));
      line[l] = 0;
      client.find((char*) "\r\n\r\n");
      if (strncmp_P(line, PSTR("POST"), strlen("POST")) == 0) {
        l = client.readBytes(line, sizeof(line));
        line[l] = 0;

        // parse the parameters sent by the html form
        const char* delims = "=&";
        strtok(line, delims);
        const char* ssid = strtok(NULL, delims);
        strtok(NULL, delims);
        const char* pass = strtok(NULL, delims);

        // send a response before attemting to connect to the WiFi network
        // because it will reset the SoftAP and disconnect the client station
        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Connection: close"));
        client.println(F("Refresh: 10")); // send a request after 10 seconds
        client.println();
        client.println(F("<html><body><h3>Configuration AP</h3><br>connecting...</body></html>"));
        client.stop();

        Serial.println();
        Serial.print("Attempting to connect to WPA SSID: ");
        Serial.println(ssid);
        WiFi.persistent(true);
        WiFi.setAutoConnect(true);
        WiFi.begin(ssid, pass);
        WiFi.waitForConnectResult();

        // configuration continues with the next request

      } else {

        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Connection: close"));
        client.println();
        client.println(F("<html><body><h3>Configuration AP</h3><br>"));

        int status = WiFi.status();
        if (status == WL_CONNECTED) {
          client.println(F("Connection successful. Ending AP."));
        } else {
          client.println(F("<form action='/' method='POST'>WiFi connection failed. Enter valid parameters, please.<br><br>"));
          client.println(F("SSID:<br><input type='text' name='i'><br>"));
          client.println(F("Password:<br><input type='password' name='p'><br><br>"));
          client.println(F("<input type='submit' value='Submit'></form>"));
        }
        client.println(F("</body></html>"));
        client.stop();

        if (status == WL_CONNECTED) {
          delay(1000); // to let the SDK finish the communication
          Serial.println("Connection successful. Ending AP.");
          configWebServer.stop();
          WiFi.mode(WIFI_STA);
          return;
        }
      }
    }
  }
}
1
  • Using second request is such a nice, simple answer. Works flawlessly! Dec 16, 2022 at 0:14
1

There are several intertwined issues in this one.

Firstly, setup() has some code running before it should be.

That first problem is here:

  server.on("/authenticate", HTTP_POST, onSetCredentials);
  server.begin();
  
  performWifiConnect(ssid, password);
  WiFi.waitForConnectResult(CONNECTION_TIMEOUT);

You've already set ssid and password to be null, and server.begin() won't run the server yet (I'll get to that issue in a minute), so this is setting your credentials to be null at the start of the program. There's no point in running waitForConnectResult() then, because there is nothing to connect. There may be logic in setting performWifiConnect() though, since if you've ever set the Wi-Fi credentials with wifi.begin() before turning off persistent, those credentials are still remembered (it just won't remember new ones).

The next issue is that you're only handling server requests while the connection is not invalid. Thus, when there is an invalid credential for the network, the server doesn't run, and you don't get a response.

void loop() {
  if (state == STATE_DEFAULT) {
    server.handleClient();
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected!");
    }
  }

  else if (state == STATE_CREDS) {
    whileConnecting();
  }
  
  delay(DELAY);
}

As you can see, server.handleclient() (the actual server; the ability to respond to requests) is only running while it does not have state == STATE_DEFAULT. That is then changed, state = STATE_CREDS;, during the callback for getting network credentials. You'll probably get a reply from the request handler that's currently running, but this will otherwise cause you problems since the server then never responds to any future requests. You must continually call server.handleclient() or nothing server-related will work.

But, something else is also wrong. You can only pass replies back from the server if you actually send them while the server is still processing a request.

else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    state = STATE_CREDS;
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
  }

As you can see, there is no server.send() in this branch, so you'll get no reply. This is likely the most immediate cause of your problem with an empty response.

Instead, you have that in the function whileConnecting(). As noted earlier, the server cannot send replies that aren't in the function it calls when it gets a request, or in functions called by that function. The server does not call whileConnecting(), loop() does. So it can't see or send that reply.

(Side note: This requirement of the server is implemented pretty weirdly, since you're calling server methods while one of its methods calls your code, only for the server to then ask itself what you told it while it was busy with your request, grab that buffer, and return it to the client. But it only does that check somewhere inside server.handleClient(), and other send() calls will either be ignored or get queued to send the next time the client makes a request.)

If you were to wait for a connection inside the server's request handler instead, and then send the error/success messages there too, this might work. It's still bad practice since the server (that part I said you always want to not hold up) and the entire rest of your code will be on hold while it waits to connect, but you'll probably have it work as long as there's nothing else time-critical or important going on.

The best approach would be to have your client poll a second address (or just the same address with no POST values, then have the server check whether there are no args) and have that say whether it is currently connected.

On that note, your connection checking code also has a problem. loop() has a delay in it, which is already going to bog things down. But you also have the whileConnecting function check for elapsed time. This is the proper way to coordinate multiple independent check-back-and-do-stuff operations, so you're aware of it, but this hasn't been transferred to the rest of the code.

Except, also in whileConnecting, you then call WiFi.waitForConnectResult(CONNECTION_TIMEOUT);. This will block that function until a result is present, which thus halts everything else for however long that timeout is. The only place you "should" use this (again, probably bad practice) is inside the server if you were trying to have it directly reply with the connection status or if there's not anything else important going on (which there is, so this is an issue). Since you're already running code periodically on a timer, just have this code check for the timeout directly (easiest method: call the connect function with the credentials, set the check-back time by timeout, then check again), and just reconnect then instead of forcing a wait. This will leave the server and the rest of your code responsive.

Now, with all (well, most) of that put together: (NOTE: This code will need some changes made to the rest of your code, and may require adjustment itself, but it should get you very close. I suggest making a backup copy of your code first in case you need to reference or fix something. I also wrote this in a text editor, as I don't currently have access to the IDE to test this. There might be minor errors.)

Solution A (have the server wait and tell you the results): (NOTE: also, delete whileConnecting() and all the state if you use this method; you want to make sure server.handleClient() is always being called until the connection is actually successful, or just plain always.)

void onSetCredentials() {
  Serial.println("onSetCredentials");
  
  server.arg("ssid").toCharArray(ssidNew, 64);
  server.arg("password").toCharArray(passNew, 64);

  if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
    Serial.println("Already connected to " + String(ssidNew));
    server.send(200, "application/json", "{ \"success\": true, \"changed\": false }");
  } else {
    Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
    performWifiConnect(ssidNew, passNew);
    startingMillisConnection = millis();
    //code to detect the server connection status and respond starts here//
    WiFi.waitForConnectResult(CONNECTION_TIMEOUT);
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Connected to " + String(ssidNew) + " successfully!");
      
      strcpy(ssid, ssidNew);
      strcpy(password, passNew);
      
      server.send(200, "application/json", "{ \"success\": true, \"changed\": true }");
    }
    
    // Failure
    if (currentMillis - startingMillisConnection >= CONNECTION_TIMEOUT) {
      Serial.println("connection failed, attempting to reconnect to old network!");
      Serial.println("ssidOld: " + String(ssid));
      Serial.println("passOld: " + String(password));

      String errorMessage = "Timed out while attempting to connect to " + String(ssidNew);
      Serial.println(errorMessage);
      server.send(500, "application/json", "{\"errorMessage\":\"" + errorMessage + "\"}");
    }
  }
}

This solution (A) will probably give you the connection status back, assuming the Wi-Fi connection attempt times out before the cURL request/server does. I haven't actually tested this and I did mention it's kinda bad practice.

Solution B (Poll the server for connection status rather than waiting for a reply): (Note: Again, delete any state logic and whileConnecting() from the other code.)

void onSetCredentials() {
  Serial.println("onSetCredentials");
  if (!server.hasArg("ssid")){ //check for no-arg request and return conn status//
    server.send("200", "text/plain", (WiFi.status() != WL_CONNECTED)? "Connected" : "Not Connected"); //or whatever other message(s) you want
    //You may also want to add some sort of Serial.print()-type logging here too, since I didn't.//
  }
  else{ //existing connection setup request handler resumes here//
    server.arg("ssid").toCharArray(ssidNew, 64);
    server.arg("password").toCharArray(passNew, 64);

    if (String(ssidNew) == String(ssid) && WiFi.status() == WL_CONNECTED) {
      Serial.println("Already connected to " + String(ssidNew));
    } else {
      Serial.println("Switching from " + String(ssid) + " to " + String(ssidNew));
      performWifiConnect(ssidNew, passNew);
      startingMillisConnection = millis();
    }
  }
}

With Solution B, you can call the cURL command to the ESP8266 without the ssid and password set, and you'll get a connection status back instead. This may take a few attempts while it's still in the process of connecting, but if it remains not connected, you know the credentials are wrong.

4
  • If you want to have a web page poll this server using solution B, I suggest the meta refresh HTML tag (note that this will also clear any forms on the current page when it refreshes, so use a frame or have this on a general status page without forms) or the JavaScript "setTimeout" function to run code later/on a schedule.
    – user47164
    Dec 15, 2022 at 14:38
  • I'm not sure if all of this is true. For example, I can indeed reply to the request from another function. And I think the reason for this is precisely because I don't call handleClient() while the state is STATE_CREDS. I do want this to be blocking. The only reason I used the timer method is because, as I mentioned, some people suggested to leave this connection non-blocking, but it did not work either way. Upvoted because I appreciate the time you took to write this all out, even though it didn't solve my issue. I think @Juraj's 2-request method is the way to go. Dec 16, 2022 at 0:13
  • That's quite fair. I hadn't seen the other solution (I had the page loaded the previous day, then worked on it at a more sane hour) and it is the "proper" solution to this without side effects. Hopefully the details help, though? Well, except for the part where I was wrong about being able to reply from another function; I'll have to look into that because I was unaware of that feature...
    – user47164
    Dec 16, 2022 at 5:03
  • Yea I mean, if anything it confirms that there's really no way to do it, other than making multiple calls, one before the switch and one after. Dec 16, 2022 at 15:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.