Using HTTP requests to run different programs without blocking the CPU on ESP8266

Hello. I have been tinkering with a project for the past 3 or 4 weeks and am currently hitting a brick wall and wondering if someone can point me in the right direction or tell me that I'm doing it completely wrong :upside_down_face:.

My end goal of this project is to have an ESP8266 respond to an HTTP request which will run some effects on 5meters of RGB LED strip using the Adafruit NeoPixel library (or similar library).

Each different HTTP request/URL should trigger a different effect. And the "effect" will repeat over and over until a different HTTP request/URL is triggered externally (I have a system for that already).

My sketch currently is just using a breadboard and a couple of LEDs as a proof of concept before moving on to the full Adafruit library and connecting the LED strip.

The code below successfully connects to WiFi, get's an IP address, and runs the HTTP server.

I am able to call the URLs listed to switch the on board LED on and off successfully. And I am able to use the URL examples of /BlinkSlow and /BlinkFast to run the blink program, but it only ones the program once and doesn't repeat (as expected since there is no loop).

While running these programs/functions they block any subsequent requests over HTTP etc as they are using the "delay" command which is tying up the CPU etc. This means that a new URL cannot be triggered until the program gets to the end. Also the end goal is that the program will repeat so will need to be interrupted.

I have studied the Blink without Delay code and I have a program/function setup on /BlinkLoopA however this crashes and reboots the ESP every time the URL is triggered.

Could someone please advise where I might be going wrong with this or if this is the wrong approach?

Eventually I would like to have no more the 10 URLs on the setup, and on calling each URL from the browser it'll run (and repeat) the given effect over and over until a different URL is called to start a different effect.

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

#ifndef STASSID
#define STASSID "mywifi"
#define STAPSK  "mywifipass"
#endif

const char* ssid = STASSID;
const char* password = STAPSK;

ESP8266WebServer server(80);

const int led = 13;

// The root website page that is displayed when browsing to the IP of the ESP over the local network
void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/plain", "The web server is running\r\n");
  digitalWrite(led, 0);
}

//Display error if URL isn't found
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);
}

//BlinkLoopA test without delay code
int ledStateA = LOW;
unsigned long previousMillisA = 0;
const long intervalA = 500;
const unsigned int LED_PINA    = 5;


void setup(void) {
  pinMode(LED_PINA, OUTPUT);
  digitalWrite(led, 0);
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  pinMode(LED_BUILTIN, OUTPUT);     // Initialize the LED_BUILTIN pin as an output

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print connection details
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp8266")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);

  // Switch the Onboard LED On
  server.on("/led-on", []() {
    server.send(200, "text/plain", "LED is ON");
    digitalWrite(LED_BUILTIN, LOW);   // Turn the LED on
    Serial.print("LED is ON ");
  });

  // Switch the onboard LED Off
  server.on("/led-off", []() {
    server.send(200, "text/plain", "LED is OFF");
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.print("LED is OFF ");
  });

  // Actions for various URLs that will be called by HTTP and trigger the "Programs" listed below.
  server.on("/BlinkSlow", HTTP_GET, handleBlinkSlow);
  server.on("/BlinkFast", HTTP_GET, handleBlinkFast);
  server.on("/BlinkLoopA", HTTP_GET, handleBlinkLoopA);
  server.on("/ProgramB", HTTP_GET, handleProgramB);

  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  MDNS.update();
}


// An example of a program using "delay" that will blink the onboard LED every 5 seconds for 3 times and then stop.
void handleBlinkSlow() {
  Serial.println("...................");
  Serial.println("BlinkSlow started");
  server.send(200, "text/plain", "BlinkSlow Starting");
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK ON\n");
  delay(5000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK OFF\n"); 
  delay(5000);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK ON\n");
  delay(5000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK OFF\n"); 
  delay(5000);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK ON\n");
  delay(5000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK OFF\n"); 
  delay(5000);
};

// An example of a program using "delay" that will blink the onboard LED every 1 seconds for 3 times and then stop.
void handleBlinkFast() {
  Serial.println("...................");
  Serial.println("BlinkFast started");
  server.send(200, "text/plain", "BlinkFast Starting");
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK FAST ON\n");
  delay(1000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK FAST OFF\n"); 
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK FAST ON\n");
  delay(1000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK FAST OFF\n"); 
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.print("LED is BLINK FAST ON\n");
  delay(1000);  
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("LED is BLINK FAST OFF\n"); 
  delay(1000);
};

// An exmaple of a BlinkLoop without using Delay - this program currently causes the ESP to crash
void handleBlinkLoopA() {
  Serial.println("handleBlinkLoopA started");
  server.send(200, "text/plain", "handleBlinkLoopA started");
  for(;;) {
    unsigned long currentMillisA = millis();
      if (currentMillisA - previousMillisA >= intervalA) {
        previousMillisA = currentMillisA;
        if (ledStateA == LOW) {
          ledStateA = HIGH;  // Note that this switches the LED *off*
        } else {
          ledStateA = LOW;  // Note that this switches the LED *on*
        }
        digitalWrite(LED_PINA, ledStateA);
      }
  }
}

//An placeholder for future "programs".
void handleProgramB() {
  // Repeat some code here without blocking other URLs to be called (which would stop this "program").
}

If your ESP8266 has the OS freeRTOS. You can use, with a single cpu, the OS as a state machine manager.

Or.

You can implement a sate machine scheme. Use the search and use words like "state machine" to get the deets on the matter.

My ESP8266 is part of the NodeMCU board that I have.
For testing I have both NodeMCU and Wemos D1 Mini - both are ESP8266 based.

This is always gonna be a problem. No matter what you do, the code runs line by line. If I'm correct ESP8266 is single-core. So you cannot run stuff simultaneously anyways.

What you could do is using ESP8266 to handle HTTP requests and use another board to run your programs (Blink, Slow Blink, etc.).

Example:
You can use a ATTINY85 board (or any other board you like) to run your programs and use ESP8266 to control them via the ESP8266 web server.

Maybe someone will come up with a better idea. Hope you'll get the answers. Good Luck!

your code has to get rid of all delay()-commands.

Your use of blink without delay is again blocking through the for (;:wink: loop
non-blocking programming means there is only one loop.

function loop() itself.

loop() is looping but nothing else
and all repeating must be done through repeated calls to functions inside function loop()

void loop() {
  server.handleClient();
  DoOneBlinkStep();
  DoSmethingOther();
  DoSomethingThird()

and let do function loop() the repeated call to these functions
DoOneBlinkStep();
DoSmethingOther();
DoSomethingThird()

which results in simultaniously "looping" of all functions.

This is the basic principle
for running different effects if-conditions must be added
or a switch-case-structure.

As you are demanding to have a somehow advanced functionality
somehow "advanced" programming-teqniques must be used which will take some time to learn.

I recommend to

NOT

apply these advanced programming-techniques to your existing code as the next step

For most brains this is too much new things at once.

use a switch-case-structure on a very simplified code that simply does print out different words
to the serial monitor

same thing for non-blocking timing.
First use a simplified code to learn how non-blocking timing works
This will take additional time.

If you try to apply it immediately to your existing code you need additional time and additional frustration in searching for bugs in your code.

So first steps are
write a function that does a single inverting of your LED
if on then switch to off and vice versa

use this function with the blink without delay-demo-code
to create the functionality of blinking the non-blocking way

best regards Stefan
I'm not an expert about http and server/clients maybe using an asynchronious server has advantages

The ESP32 runs an Asynchronous server.

OK – I can’t stand it anymore. I have been lurking in the forums for the last couple of weeks, but so far haven’t commented. I keep seeing the same suggestions over and over get rid of delays, use only one loop, read about state machines, etc. I have refrained from commenting because others have provided solutions or the person is so far from reality that the situation is hopeless.

This post is a bit different. The OP has a fairly clear definition of what they are trying to, has actually tried some things and the demo is manageable. They seem to lack some knowledge – that can be fixed.

First up the ESP8266 is much more complex than an UNO. The fact that you can use the Arduino IDE gives a false sense that it is easy. The ESP8266 has a lot of code running in the background to get a wireless connection, handle the TCP/IP stack, handle asynchronous browser requests, maintain network communication, etc. It shares the processor with the user code, but the housekeeping code must be run on a regular basis (every 10’s of milliseconds). This is done by sharing the processor with the user code. At the start of each loop() iteration control is given to the esp8266 code, it does necessary housekeeping then continues with the user code. Some other function calls also call the housekeeping code before returning to the user. For example delay() (this is why adding a delay sometimes “fixes” a crash). There is a watchdog timer set that causes an exception if it times out. This timer is reset by the background code. If the background code is not called often enough it times out and the system crashes. This is what happened in the handleBlinkLoopA() code - it is in an infinite loop which times out. If you need a long loop you should call yield() occasionally to give the background code an opportunity to run.

I wrote a demo program which hopefully does what the OP was trying to prototype. I assumed that there was not a real need to use a different URL for each pattern, so just did the selection as radio buttons on the home web page. Hopefully the code comments will be sufficient to describe the logic. I have never used a NeoPixel display, but I expect the pattern definition is more complex than a simple array of times.

/* ***********************************************************************************************
 * 
 * oldcurmudgeon November 6 2021
 * 
 * This sketch is for an ESP8266 to provide a web interface for blinking an LED
 *
 * The onboard LED is used so no additional hardware is needed
 *
 * It is implemented as 2 state machines. The first has the state defined by the pattern
 * being run. The state is defined by currentPattern. The state transition is done via 
 * a browser request changing newState to define the next state.
 *
 * The second state machine defines the step in the pattern of blinks. The state transition
 * occurs when the current time for the LED state expires. When the time is up the LED toggles
 * to the other state and the time for that state starts. At the end of the pattern it is
 * started over from the beginning. 
 * 
 *************************************************************************************************/ 


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

const char* ssid = "your_ssid";
const char* password = "your_pw";

ESP8266WebServer server(80);

#include <string.h>
#include <Streaming.h>   //  from "Streaming by Mikal Hart" in library manager

// Pattern selection state machine variables
volatile uint16_t newPattern = 0;  //default to first pattern volatile because it is set asynchronously
uint16_t currentPattern = 0; // default first pattern

// Pattern step state machine variables
byte step = 0;                // current step in pattern default to start
unsigned long currentTime;   // current time for test
unsigned long startTime;     // start time of this step

// led on state - the onboard led is active low (when the pin is low the led
// is on). If you are using some other pin to drive the led so that the led is on when
// the pin is high this should be changed to one.
const byte LED_ON = 0; 
byte ledState = LED_ON;     // current led state default on

// led patterns - this is a series of numbers defining the led on and off times
// This example is 4 patterns of 6 changes. A zero before the end of the pattern
// indicates a short pattern that restarts when it detects a zero.
const byte NUM_PATTERNS = 4;    // number of patterns 
const byte PATTERN_LEN = 6;    // pattern length
const unsigned long pattern[ NUM_PATTERNS][PATTERN_LEN] = 
        {{200,200,200,200,200,200},
        {1500,1500,1500,1500,1500,01500},
        {1500,1500,200,200,0,0},           // short pattern example 
        {1500,1500,1500,1500,200,200}};
/* ***************************************************************************
*
* Handle root web page
*
******************************************************************************/
void handleRoot() {
  if (server.hasArg("pattern")){ // the browser has sent a response
    // Save new value from web page browser response  
    newPattern = server.arg("pattern").toInt();
  }
   /* Build html page
    * 
    * The new lines (\n) are not required by the browser - they are formatting for a human 
    * looking at the page source on the browser
    * 
    * The "String((newPattern == x) ? " checked" : " ")"  construct is used to set the
    * pattern selected as checked when the web page is refreshed. If newPage is equal to
    * the value "checked" is added to the attributes, otherwise a blank is added. You can
    * see the result on the web page by right-click then select "View Page Source".
    * This will show the actual HTML sent to the browser.
   */ 
  String htmlPage =
    String("<!DOCTYPE HTML>\n") 
    + "<html lang='en'>\n"
    + "<head>\n"
    + "<!-- Ignore request for nonexistent favicon -->\n"
    + "<link rel='icon' href='data:,'>\n"
    + " <meta name='viewport' content='width=device-width, initial-scale=1.0'>"
    +"</head>\n"
    + "<body>\n\n"
    + "<h1 style='text-align: center;'>LED Pattern Blinker</h1>\n"
    + "<div>\n"
    + "<form style='font-size: 20px'>\n"
    +   " <input type='radio' name='pattern' value='0' required"
    +  String((newPattern == 0) ? " checked" : " ")        
    +   " > Short blink<br>\n"
    +   " <input type='radio' name='pattern' value='1' required"
    +  String((newPattern == 1) ? " checked" : " ") 
    +   "> Long blink<br>\n"
    +   " <input type='radio' name='pattern' value='2' required"
    +  String((newPattern == 2) ? " checked" : " ") 
    +   "> Long Short blink <br>\n"
    +   " <input type='radio' name='pattern' value='3' required"
    +  String((newPattern == 3) ? " checked" : " ") 
    +   "> Long Long Short blink<br>\n"
    +   "<input type='submit' value='Set Pattern'>\n"
    +   "</div>\n"
    +"</body>\n</html>\n";

  server.send(200, "text/html",htmlPage);
    
}

void handleNotFound() {
    // no clue what they sent
    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);

}

void setup() {
  pinMode(LED_BUILTIN,OUTPUT);

  Serial.begin(19200);
   

  // Set up WiFi network
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp8266")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  server.onNotFound(handleNotFound);
  
  // Start the server
  server.begin();
  
  startTime = millis();      // get current time for start of pattern
}

void checkPattern() {
    if (newPattern != currentPattern){     // browser has sent a new pattern
        // switch to new pattern
        ledState = LED_ON;  // start with led on
        digitalWrite(LED_BUILTIN,ledState);  // start with led on
        step = 0;   // start at beginning of pattern
        currentPattern = newPattern;  // switch to new pattern
        startTime = millis();      // get current time for start of pattern
    } 
};    

void loop() {
  server.handleClient();      // check for connection request
  
  // Check to see if there is a new pattern request. If the check is done here
  // the new pattern starts immediately. If you want the new pattern to start 
  // after the current pattern has completed, comment out the line below and uncomment
  // the one in the end of pattern processing
  checkPattern();
  
  currentTime = millis();      // get current time for compare
  if (currentTime  - startTime > pattern[currentPattern][step]) {
     // done with this step 
     ledState ^= 1;  // toggle led state
     digitalWrite(LED_BUILTIN,ledState);  // change led state       
     step++;             // next step
     startTime = currentTime;  // start next led pattern step time
     if ((step >= PATTERN_LEN)                    // end of pattern
         || (pattern[currentPattern][step]==0)){  // or short pattern
        // end of pattern processing     
        step = 0;   //restart pattern
        
        // Check to see if there is a new pattern request. If the check 
        // is done here the new pattern starts after the current pattern
        // has completed. (Remember to comment out the call above.)
       // checkPattern();        
     }
  }     
}