ESP-01 inconsistent WiFi performance

Hello,

I have connected a Pi Pico to an ESP-01S module using Serial1. I am using the ESP-01S module with the AT-commands, so that I am able to provide the HTML in the Arduino Code (I am using the Arduino mBed Core), as well as read the request URLs.

The code is fairly simple as it stands now. Inside setup() I run the AT-commands necessary to setup the ESP-01S as a server, and inside loop, either the HTML is served (for the / request), or the onboard LED is switched on of off depending on the URL ("/ON" and “/OFF”).

The problem is the inconsistency in the performance I get for my requests (which are XHR-requests from a browser). Sometimes I get a response in 60ms, usually I get around 500ms, but sudden peaks up to and above 2s happens frequently.

Are there optimisations I can do to the code, or is this to me expected using this, rather cheap, ESP-01S module?

int rx = 21;
int tx = 20;
int enable = 22;
int led = 25;
String connectionId = "";

void setup() {
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
  
  Serial1.begin(115200);

  Serial.begin(9600);

  Serial.println("Toggling Enabled");
  pinMode(enable, OUTPUT);
  digitalWrite(enable, HIGH);
  delay(1000);
  digitalWrite(enable, LOW);
  delay(1000);
  digitalWrite(enable, HIGH);
  delay(1000);

  Serial.println("Enable toggled");
  Serial.println("Starting Wifi");


  Serial1.write("AT+RST\r\n");
  delay(150);
  Serial1.write("AT+CWMODE=3\r\n");
  delay(150);
  Serial1.write("AT+CWSAP=\"ESP-01\",\"PASSORD\",1,0,4,0\r\n");
  delay(150);
  Serial1.write("AT+CWLIF\r\n");
  delay(150);
  Serial1.write("AT+CIPMUX=1\r\n");
  delay(150);
  Serial1.write("AT+CIPSERVERMAXCONN=1\r\n");
  delay(150);
  Serial1.write("AT+CIPSERVER=1,80\r\n");
  delay(150);  
  Serial1.write("AT+CIPSTO=2\r\n");
  delay(150);  
  Serial.println("WiFi Started");
}

void loop() {
  
  if (Serial.available()) {
    Serial1.write(Serial.read());
  }

  if (Serial1.available()) {
    String inputString = Serial1.readStringUntil('\n');
    Serial.println(inputString);
    
    if (strstr(inputString.c_str(), "+IPD")) {
      //find connectionId
      Serial.print("input: ");
      Serial.println(inputString);
      
      int firstComma = inputString.indexOf(',');
      int secondComma = inputString.indexOf(',', firstComma+1);
      connectionId = inputString.substring(firstComma+1, secondComma);
      Serial.print("connectionId: ");
      Serial.println(connectionId);
    
      if (connectionId != "" && strstr(inputString.c_str(), "GET / HTTP")) {
        //serve index.html  
        espsendHtml("<button onclick=\"fetch('/ON');\">ON</button><button onclick=\"fetch('/OFF');\">OFF</button>");
      } else if (connectionId != "" && strstr(inputString.c_str(), "GET /ON HTTP")) {
        digitalWrite(led, HIGH);
        espSendData("OK");
      } else if (connectionId != "" && strstr(inputString.c_str(), "GET /OFF HTTP")) {
        digitalWrite(led, LOW);
        espSendData("OK");
      }
      
      String closeCommand = "AT+CIPCLOSE="; 
      closeCommand+=connectionId; // append connection id
      closeCommand+="\r\n";
      sendData(closeCommand,50,true);  
    }

    inputString = "";
    
    /*if(Serial1.find("+IPD,")) {
     delay(300);
     connectionId = Serial1.read()-48;
     Serial.print("connectionID: ");
     Serial.println(connectionId);

     if (Serial1.find("GET /ON")) {
      digitalWrite(led, HIGH);
      espsend("OK");
     } else if (Serial1.find("GET /OFF")) {
      digitalWrite(led, LOW);
      espsend("OK");
     } else {
      espsendHtml("<button onclick=\"fetch('/ON');\">ON</button><button onclick=\"fetch('/OFF');\">OFF</button>");
     }
     
      String closeCommand = "AT+CIPCLOSE=";  ////////////////close the socket connection////esp command 
     closeCommand+=connectionId; // append connection id
     closeCommand+="\r\n";
     sendData(closeCommand,300,true);
    }*/
  }
}

void espSendData(String data) {
  String header = "HTTP/1.1 200 OK\r\n";
  header += "Content-Length: ";
  header += (data.length());
  header += "\r\n";
  header += "Connection: close\r\n\r\n";

  String msg = header + data;
  espsend(msg);
}

void espsendHtml(String html) {
  String body = "<html><head></head><body>";
  body += html;
  body += "</body></html>";

  String header = "HTTP/1.1 200 OK\r\n";
  header += "Content-Length: ";
  header += (body.length());
  header += "\r\n";
  header += "Connection: close\r\n\r\n";

  String msg = header + body;
  espsend(msg);
}

void espsend(String d){
     String cipSend = " AT+CIPSEND=";
     cipSend += connectionId; 
     cipSend += ",";
     cipSend +=d.length();
     cipSend +="\r\n";
     sendData(cipSend,50,true);
     sendData(d,50,true); 
 }

void sendData(String command, const int timeout, boolean debug) {
  Serial1.print(command);
  delay(timeout);
}```

You didn’t say how you power the ESP. They draw ~80mA with 400mA peaks.
Not sure if the supply of that board can provide that.
But why questions for a Raspberry Pi pico on an Arduino forum…
Leo…

@Wawa The Raspberry Pi Pico has support from Arduino. It is part of the development for the new Arduino RP2040 or whatever it is going to be called. The board uses the same chip developed by the Raspberry team.

“Cheap” has absolutely nothing to do with it. The ESP8266 - the same chip irrespective of whichever ESP-xx module you have - is extremely capable, limited only by the code you write.

Using it via the “AT” software is however severely limiting. In general, you want to run all your 'net code on the ESP itself. Teaming it with a substantially less capable Arduino is only making things difficult, however the Pi Pico is apparently - I was not previously aware of it - quite capable in itself.

So I think the message here is to program the ESP-01 to do whatever you need and if you need to use the Pico for some other control function then do that.

To program the ESP-01, you use a purpose-built USB programming adapter:
Aliexpress item
At least, that is the manual switching version; here is the automatic version which while it does have the reset button, initiates the programming directly from the Arduino IDE:
Aliexpress item
Similarly, to actually use the ESP-01 in a project, a very convenient way is with the cheaper adapter board, plugged into a USB “phone charger” so you have both the 5 V and 3.3 V conveniently and adequately supplied.
Aliexpress item
You have three GPIO conveniently available by soldering to the adapter, and the fourth - serial Rx - can be separated from the USB chip if necessary by cutting a track. Using those I/O you can connect port expanders for many more I/O connections and a variety of sensors.

Why two processors. Why a programmer.
Can’t you do whatever you’re doing with one board, like the WeMos D1 mini.
Or an ESP32 variant if you want more IO.
Leo…

Well, yes of course. :grinning: My point as always is that the ESP-01 and especially the “S” version is in itself very capable and readily expanded using I²C.

I do realise that it is not really substantially cheaper than the WeMOS D1 Mini, so it would not be the one to pick for a project except for its small size (in which case you might consider removing the pin header) - it will run nicely on a pair of alkaline “AA” or a lithium cell. But the OP here already has the ESP-01S, so my comments are addressed to that. :sunglasses:

Back to the issue,

I’d say 300mA peaks, particularly when connecting to WiFi, but yes this may have something to do with it.

Totally, although you can do almost everything using AT-commands, it is rather cumbersome. You effectively use the ESP as an interpreter, which you control with compiled code on a different device.
Programming an ESP-01 directly is a lot easier.

Is that a 3.3v board as well ? how have you connected it ?

Or just about every arduino on which you run an empty sketch (or blink, anything really as long as the UART isn’t used in the sketch) and pull the ESP-01’s GPIO 0 ‘LOW’ at boot to put it in flash mode.

Thank you for your answer Wawa. I haven’t checked the power draw yet. I have connected it to the 3.3V output of the Pi Pico, which might be able to provide up to 300 mA, according to the manual. I will, however, try to power the ESP-01 module from two AA batteries to check if that helps.

From the manual:
“3V3 is the main 3.3V supply to RP2040 and its I/O, generated by the on-board SMPS. This pin can be used to power external circuitry (maximum output current will depend on RP2040 load and VSYS voltage, it is recommended to keep the load on this pin less than 300mA).”

I do realise that the ESP-01 can be programmed with all of the networking specifically. However, I am running a summer course for children and I am hoping to use the Pi Pico as a microcontroller for a set of RC boats that we are building. I have developed drag-and-drop programming support for the Pi Pico using a web browser.

Instead of relying on a separate module, I was thinking that this ESP-01 module could be used as an affordable means to control the boats from smartphones. I don’t need, nor expect, millisecond turnarounds for the WiFi, but two seconds might be a bit hard to control still :slight_smile:

Thank you for your reply @Deva_rishi!

The Pico might be able to provide 300mA on the 3.3V pin. But I will retest with an external power supply for the ESP-01S module.

I do realise that using it with AT software is limiting. I might try to pull the GPIO 0 to low and see if I would be able to load a sketch onto the ESP-01 that will make it having to do less work per request.

For my purpose, though, I also can program the ESP-01 modules up-front and write some sort of protocol for the transfer between the Pico and the ESP-01.

There are also Bluetooth modules, but I quite like this ESP-01 module both for its size and for its price.

There is also the Arduino Nano 2040 Connect device, but I am not sure this will come out in time for me to do the up-front planning for my summer event :slight_smile: The 2040 Connect device will also do something similar, with an on-board ESP32 in the on-board u-blox module.

Are you aware that in this case the typo has the effect of it no longer being a valid password (only 7 character where 8 is the minimum)

I never use the AT-commands anymore, but i have not had those long response times ever that i remember, other than when the ESP loses it’s wifi connection.

Only the range should be a limiting factor as with any wireless system, and the power hungry nature is another drawback.

That’s probably a good idea.

So it’s a 3.3v board ?

Those are a lot less power hungry, but less capable, and require some kind of software on the controlling device (either android or IOS) where as the ESP only requires a browser.

Thank you for the reply @Deva_Rishi!

I was not aware that the missing “W” in the password would cause it to create an open AP. I was wondering why this was the case, though, but I had left investigating this to later :slight_smile: (A fun aside the word “Passord” is the Norwegian equivalent of the English word “password”).

Maybe the module requires more current to operate fully. I will re-test, this evening when I get back home.

Yes, the Pi Pico is a 3.3V device.

And I agree with your rationale, and this is why I found the ESP-01 so appealing, as it won’t need anything other than a browser on the other end to interface with the module.

Yes, you can do that if it suits to “bodgie” up the wiring, figure out the logic level translation and fiddle around with pulling GPIO0 LOW, but for just a few dollars, the programmer modules make it so much simpler! :sunglasses:

After some checking with a proper power supply, the problem is the same. I have identified one possible culprit in the line

String inputString = Serial1.readStringUntil('\n');

This seems to always time-out on the very last message sendt from the ESP-01, as it seems it is not terminated with a \n for some reason.

I have set Serial1.setTimeout(200); which helps somewhat. But I still do get long response times frequently. When testing it seems to lag in-between me clicking on the button on the website and the Serial message reaching the Pi Pico over UART.

Is it possible that the Serial input buffer overflows ? Or does the ESP just simply never sends the terminating ‘\n’ ?
In case of the latter you could attempt to update the ESP’s firmware. Still i would drop the whole AT-command approach for a method where you serve the page directly from the ESP using ESP8266 webserver, and pass much smaller amounts of data back and forth between the 2 units.

I tried to implement a webserver onto the ESP-01 itself, in order to limit the data being transferred between the two MCUs. I do still get quite high response times, which I do not understand. Maybe there is an issue with my module...

I am attaching the ESP source code as well...

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

ESP8266WebServer server(80);
String htmlContent = "";
const int led = 2;

void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/html", htmlContent);
  digitalWrite(led, 0);
}

void handleNotFound() {
  digitalWrite(led, 1);
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
  digitalWrite(led, 0);
}

void setup()
{
  pinMode(led, OUTPUT);
  digitalWrite(led, 1);
  delay(299);
  digitalWrite(led, 0);
  delay(299);
  digitalWrite(led, 1);
  delay(299);
  Serial.begin(115200);
  Serial.println();

  Serial.print("Setting soft-AP ... ");
  Serial.println(WiFi.softAP("ESP-01S", "Password") ? "Ready" : "Failed!");

  server.on("/", handleRoot);
  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("HTTP server started");
  digitalWrite(led, 0);
}

void loop()
{
  server.handleClient();

  if (Serial.available()) {
    String inputString = Serial.readStringUntil('\n');
    
    const char * inputChar = inputString.c_str();
    char inputCharArray[100];
    inputString.toCharArray(inputCharArray, 100);

    String buttonValues[8];
     int index = 0;
    if (strstr(inputChar, "+B")) {
      char* command = strtok(inputCharArray, ";");
      while (command != NULL) {
        buttonValues[index] = command;
        index++;
        command = strtok(NULL, ";");
      }

      if (index == 8) {
        String returnVal = buttonValues[3];
        server.on(buttonValues[1], [returnVal]() { server.send(200, "text/plain", returnVal); });  
        htmlContent += "<button style=\"position:absolute; top: " + buttonValues[5] + "px; left: " + buttonValues[4] + "px; width: " + buttonValues[6] + "px; height: " + buttonValues[7] + "px;\" onclick=\"fetch('" + buttonValues[1] + "');\">" + buttonValues[2] + "</button>";
      }
      
    }
  }
}

There are a few things in your code that are not correct.
I'll go through them one by one in order of appearance.

String htmlContent = "";
const int led = 2;   // the builtin led on most ESP-01's is on pin 1, but that is also TX
                              // just make sure that also for GPIO 2 you need to make the LED active LOW

void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/html", htmlContent);
  digitalWrite(led, 0);
}

To declare htmlContent as a global String is not a wise idea, although this not the cause of slow response times, it may eat up memory at some point, and that should be prevented. It is better to gather data and store that in variables, and then within the callback function create a 'String' with html header, body etc. and send that, and the String will be destroyed when it goes out of scope, more the way the handleNotFound() callback is setup.

  if (Serial.available()) {
    String inputString = Serial.readStringUntil('\n');
    

This is a problem, now if there are any characters in the Serial input buffer, the program is held up until it finds a '\r'
This is when your webserver is no longer responsive. You then do some form of parsing copying data into 2 separate c-strings, all fine, but the 'readStringUntil()' is blocking code.
It does depend a little on what you send of course, but if you would have sent something like

Serial.print("This is the message\n\r"); 

your sketch will be held up until the next time a message arrives, and the browser will not wait that long for a response.

server.on(buttonValues[1], [returnVal]() { server.send(200, "text/plain", returnVal); });

This just doesn't belong inside of loop() but in setup().
You should declare all callbacks in setup(), if you want the function to respond in a different way later, you can do that within the callback function itself.

There are many ways to receive Serial data, and Serial.readStringUntil() is probably the worst of them.
Serial Input Basics contains very reliable and 'non-blocking' methods of Serial communication
Fact is that i don't think you really need to receive Serial data at this end at all.
The ESP should be in charge, it will contain the UI and it has the faster processor and bigger memory (I think, i don't know the exact specs of the pico)

Probably not.
I suggest you simplify the webserver so the main page has 2 forms (or just 1) extracts the data from the request (either post or get) using the .hasArg() and arg() function and sends a message through the UART to either the pico or the Serial monitor (or though the pico to the monitor, whatever) To confirm that there is nothing wrong with your unit.
I was looking for a clear example, but somehow not only do all of my examples have a lot of not relevant code, but also the examples that come with the library do have a lot of code that only makes things more complicated.
Either way, the webserver responds to a request the moment it encounters a

server.handleClient();

Thank you for your reply @Deva_Rishi!

I have made sure that I only send lines through the Serial with a "\n" at the end. But I will look more into this. I will read through the link provides (the Serial Input Basics).

Regarding the server.on function calls, I think that I do need them to be where they are. Because the way I am setting it up, I would like to module to accept a String via Serial to add a new button the the UI. It does this when the String is received over Serial:

+B;URL;Button click URL;Label;Serial Return String;Left pos;Top pos;width;height

This way I can pre-program the module itself, while the student (which is programming the Pico only) can still choose which buttons to display, and where. I should extract it to a separate function, to make this clear, though.

I did, however, test setting it up on my local WiFi network (instead of hosting its own access point), and this improved the performance a lot! Setting it up this way, most calls where sub-100ms, whereas with the AP configuration, almost all calls where 500ms or more...

Well maybe, but it tend to do that in another way.
Anyway i got inspired out of frustration i didn't have a simple example so

#include <ESP8266WiFi.h>
#include <WiFiClient.h> 
#include <ESP8266WebServer.h>

//#include <PatchDNSServer.h>


#define LEDPIN 2
#define DNS_PORT 53

const char
*pageheader = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">",
 *htmlhead = "<html><head><title>ESP Webserver</title><meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" ></head>",
  *bodystyle = "<body style=\"color: dimgray; background-color: palegoldenrod; font-size: 12pt; font-family: sans-serif;\">",
   *htmlclose = "</body></html>";

bool ledon = true; 

DNSServer dnsServer;

ESP8266WebServer server(80);

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Configuring access point...");
  WiFi.softAP("ESP-AP","PASSWORD");

  //dnsServer.start(DNS_PORT, "myesp", IPAddress(192, 168, 4, 1));  

  pinMode(LEDPIN, OUTPUT);
  digitalWrite(LEDPIN, LOW);
  
  server.on("/", handleRoot);
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  server.handleClient();
  //dnsServer.processNextRequest();
}

void handleRoot() {
  const char* accessIP = "http://192.168.4.1";
  if (server.hasArg("led")) {
    String led = server.arg("led");
    if (led == "on") ledon = true;
    if (led == "off") ledon = false;
  }
  
  String s;
  s += pageheader;
  s += htmlhead;
  s += bodystyle;
  s += "<h2> Turn the LED on or off</h2>";
  s += "<h3> At the moment it's ";
  if (ledon) {
    s += "on";
    Serial.println("Turn Led on.");
    digitalWrite(LEDPIN, LOW);
  }
  else {
    s += "off";
    Serial.println("Turn Led off.");
    digitalWrite(LEDPIN, HIGH);
  }
  s += ".</h3>";
  s += "<form action='http://192.168.4.1' method='get' name='ledon'>";
  s += "<input type='hidden' name='led' value='on'>";
  s += "<input type='submit' value=' LED ON  '></form><br>";
  
  s += "<form action='http://192.168.4.1' method='get' name='ledoff'>";
  s += "<input type='hidden' name='led' value='off'>";
  s += "<input type='submit' value=' LED OFF '></form><br>";
  
  s += htmlclose;
  
  server.send(200, "text/html", s);
}

This is more or less how i would do it.
The lines i commented out create an alias for the AP's ip (192.168.4.1) but it some kind of modification of a library, by now probably superseded (or not) but i can't find where i got it from anymore.

connecting it to a router hardly seems like a good solution btw for what you intend to achieve.

Ah, this looks like the more recent dnsserver