ESP32 Web Server: Set Timer Schedule (Arduino IDE)

In this project, we’ll build a web server with the ESP32 that allows you to control an LED by scheduling a timer with an ON/OFF action for any period duration (seconds, minutes, or hours). The web page also has two buttons that you can use to turn the LED on or off immediately. The ESP32 will be programmed using Arduino IDE.

ESP32 Web Server Set Timer Schedule Arduino IDE

You might like reading: Building an ESP32 Web Server: The Complete Guide for Beginners (Arduino IDE).

Project Overview

The following image shows the web page you’ll build for this project.

ESP32 Web Server Set Timer Schedule Arduino IDE Core Project Overview

Here are the key features of this web server:

  • Current LED state text label: “GPIO is ON” / “GPIO is OFF”
  • Instant LED control: TURN ON / TURN OFF buttons
  • Set Timer HTML form: you can set the action (TURN ON/OFF), time unit, and timer duration
  • Timer status and remaining time to perform the selected action
  • Cancel timer button

For simplicity, we’ll save the HTML and CSS to build the web page in a variable on the Arduino sketch, but you could use the LittleFS Filesystem.

Prerequisites

Before following this guide, you need to install the ESP32 Core add-on in your Arduino IDE. Follow the next guide to install it, if you haven’t already:

You will also need an ESP32 development board model of your choice.

Install Required Libraries in Arduino IDE

We’ll build the web server using the following libraries:

You can install these libraries in the Arduino Library Manager. Open the Library Manager by clicking the Library icon in the left sidebar.

Search for ESPAsyncWebServer and install the ESPAsyncWebServer by ESP32Async.

Installing ESPAsyncWebServer ESP32 Arduino IDE

Then, install the AsyncTCP library. Search for AsyncTCP and install the AsyncTCP by ESP32Async.

Installing AsyncTCP ESP32 Arduino IDE

ESP32 Web Server: Timer Schedule – Arduino Sketch

The following code creates a web server that serves a web page that lets you schedule a timer to turn an LED ON/OFF for a selected time duration.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-web-server-timer-schedule-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <freertos/timers.h>

// REPLACE WITH YOUR NETWORK CREDENTIALS
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// LED connected to GPIO 5
const int ledPin = 5;

// Global variables
AsyncWebServer server(80);

TimerHandle_t gpioTimer = NULL;
// Timer action: 1 = ON, 0 = OFF
int targetAction = -1;

TickType_t timerStartTick = 0;
uint64_t totalDurationTicks = 0;

// HTML Web Page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <title>ESP32 Web Server - Timer Schedule</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 20px; background: #f4f4f4; }
    h1 { color: #333; }
    .card { max-width: 440px; margin: 20px auto; padding: 25px; background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
    button { width: 100%; padding: 14px; margin: 8px 0; font-size: 18px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
    .onButton { background: #28a745; color: white; }
    .offButton { background: #999999; color: white; }
    button:hover { opacity: 0.95; }
    .state { font-size: 22px; font-weight: bold; margin: 15px 0; }
    .ledOn { color: #28a745; }
    .ledOff { color: #999999; }
    label { display: block; margin: 12px 0 6px; font-weight: bold; text-align: left; }
    select, input[type="number"] { width: 100%; padding: 12px; margin: 8px 0; font-size: 16px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box;}
    .startButton { background:#1a73e8; color:white; }
    #cancelButton { display:none; background:#dc3545; color:white; padding:10px 20px; border:none; border-radius:6px; cursor:pointer; margin-top:10px; }
  </style>
</head>
<body>
  <h1>ESP32 - Timer Schedule</h1>
  <div class="card">
    <div class="state">GPIO is <span id="ledState">Loading...</span></div>
    <button class="onButton" onclick="controlLED(1)">TURN ON</button>
    <button class="offButton" onclick="controlLED(0)">TURN OFF</button>
  </div>
  <div class="card">
    <h2>Set Timer</h2>
    <form id="timerForm" action="/set-timer" method="POST">
      <label for="action">Action</label>
      <select name="action" id="action">
        <option value="" disabled selected hidden>--- Select Action ---</option>
        <option value="1">TURN ON (HIGH)</option>
        <option value="0">TURN OFF(LOW)</option>
      </select>
      <label for="unit">Time Unit</label>
      <select name="unit" id="unit">
        <option value="s">Seconds</option>
        <option value="m">Minutes</option>
        <option value="h">Hours</option>
      </select>
      <label for="duration">Duration</label>
      <input type="number" name="duration" id="duration" min="1" value="30" required>
      <button type="submit" class="startButton">START TIMER</button>
    </form>
    <div style="margin-top:20px; padding:15px; background:#f8f9fa; border-radius:8px;">
      <strong>Timer Status:</strong> <span id="status">No active timers</span><br><br>
      <span id="remaining" style="font-size:18px;"></span><br>
      <button id="cancelButton" onclick="cancelTimer()">
        CANCEL TIMER
      </button>
    </div>
  </div>

  <script>
    // Update LED State
    function updateLEDState() {
      fetch('/led-state')
        .then(r => r.json())
        .then(data => {
          const stateEl = document.getElementById('ledState');
          if (data.state === 1) {
            stateEl.innerHTML = '<span class="ledOn">ON</span>';
          } else {
            stateEl.innerHTML = '<span class="ledOff">OFF</span>';
          }
        })
        .catch(() => {
          document.getElementById('ledState').innerHTML = 'Error';
        });
    }
    // Control LED
    function controlLED(state) {
      fetch('/control?state=' + state)
        .then(() => updateLEDState());
    }

    // Timer functions
    let countdownInterval = null;
    let remainingSeconds = 0;
    // Format time to hours, minutes and seconds    
    function formatTime(seconds) {
      if (seconds <= 0) return "0s";
      let h = Math.floor(seconds / 3600);
      let m = Math.floor((seconds % 3600) / 60);
      let s = seconds % 60;
      if (h > 0) return h + "h " + m + "m " + s + "s";
      if (m > 0) return m + "m " + s + "s";
      return s + "s";
    }
    // Check the remaining time for the timer to end
    function updateTimerStatus() {
      fetch('/timer-status')
        .then(r => r.json())
        .then(data => {
          document.getElementById('status').innerText = data.status;

          const cancelButton = document.getElementById('cancelButton');
          const remainingEl = document.getElementById('remaining');

          if (data.active) {
            remainingSeconds = data.remaining_seconds;
            cancelButton.style.display = 'inline-block';

            if (!countdownInterval) {
              countdownInterval = setInterval(() => {
                if (remainingSeconds > 0) {
                  remainingSeconds--;
                  remainingEl.innerText = "Remaining: " + formatTime(remainingSeconds);
                } else {
                  clearInterval(countdownInterval);
                  countdownInterval = null;
                }
              }, 1000);
            }
            remainingEl.innerText = "Remaining: " + formatTime(remainingSeconds);
          } else {
            cancelButton.style.display = 'none';
            remainingEl.innerText = "";
            if (countdownInterval) {
              clearInterval(countdownInterval);
              countdownInterval = null;
            }
          }
        });
    }
    // Cancel timer button
    function cancelTimer() {
      if (confirm("Cancel the current timer?")) {
        fetch('/cancel-timer', { method: 'POST' })
          .then(() => updateTimerStatus());
      }
    }

    // Update every 10 seconds
    setInterval(() => {
      updateLEDState();
      updateTimerStatus();
    }, 10000);
    window.onload = () => {
      updateLEDState();
      updateTimerStatus();
    };
  </script>
</body>
</html>
)rawliteral";

// Timer Callback - Turns the LED on or off depending on the action selected
void IRAM_ATTR timerCallback(TimerHandle_t xTimer) {
  if (targetAction != -1) {
    digitalWrite(ledPin, targetAction ? HIGH : LOW);
    Serial.printf("Timer finished: GPIO %d set to %s\n", ledPin, targetAction ? "HIGH (ON)" : "LOW (OFF)");
  }
  targetAction = -1;
  timerStartTick = 0;
  totalDurationTicks = 0;
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  // Define ledPin as an OUTPUT and initialize it off (LOW)
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // Create a timer and assign the timerCallback function
  gpioTimer = xTimerCreate("GPIO_Timer", pdMS_TO_TICKS(1000), pdFALSE, 0, timerCallback);

  // Start the Wi-Fi connection
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi Connected!");
  // Print the ESP32 IP Address
  Serial.print("Access ESP32 IP Address: http://");
  Serial.println(WiFi.localIP());

  // Root URL handler
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send_P(200, "text/html", index_html);
  });

  // Instant LED control
  server.on("/control", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("state")) {
      int state = request->getParam("state")->value().toInt();
      digitalWrite(ledPin, state ? HIGH : LOW);
      Serial.printf("LED set to %s\n", state ? "ON" : "OFF");
    }
    request->send(200, "text/plain", "OK");
  });

  // Get current LED state
  server.on("/led-state", HTTP_GET, [](AsyncWebServerRequest *request) {
    int state = digitalRead(ledPin);
    request->send(200, "application/json", "{\"state\":" + String(state) + "}");
  });

  // Set the timer duration to perform the action selected
  server.on("/set-timer", HTTP_POST, [](AsyncWebServerRequest *request) {
    if (request->hasParam("action", true) && request->hasParam("duration", true) && request->hasParam("unit", true)) {

      targetAction = request->getParam("action", true)->value().toInt();
      int dur = request->getParam("duration", true)->value().toInt();
      String unit = request->getParam("unit", true)->value();

      uint64_t durationSeconds = 0;
      if (unit == "s")      durationSeconds = (uint64_t)dur;
      else if (unit == "m") durationSeconds = (uint64_t)dur * 60ULL;
      else if (unit == "h") durationSeconds = (uint64_t)dur * 3600ULL;

      if (durationSeconds > 0) {
        if (xTimerIsTimerActive(gpioTimer)) xTimerStop(gpioTimer, 0);

        uint64_t tempTicks = durationSeconds * (uint64_t)configTICK_RATE_HZ;
        TickType_t timerTicks = (tempTicks > 0xFFFFFFFFULL) ? 0xFFFFFFFFULL : (TickType_t)tempTicks;

        timerStartTick = xTaskGetTickCount();
        totalDurationTicks = tempTicks;

        xTimerChangePeriod(gpioTimer, timerTicks, 0);
        xTimerStart(gpioTimer, 0);

        Serial.printf("Timer scheduled: GPIO %d will be set to %s in %llu seconds\n", ledPin,
                      targetAction ? "ON" : "OFF", durationSeconds);
      }
    }
    request->redirect("/");
  });

  // Cancel Timer
  server.on("/cancel-timer", HTTP_POST, [](AsyncWebServerRequest *request) {
    // Stop the active timer
    if (xTimerIsTimerActive(gpioTimer)) {
      xTimerStop(gpioTimer, 0);
      Serial.println("Timer cancelled");
    }
    targetAction = -1;
    timerStartTick = 0; 
    totalDurationTicks = 0;
    request->send(200, "text/plain", "OK");
  });

  // Timer Status - Returns the remaining time in seconds
  server.on("/timer-status", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (targetAction != -1 && xTimerIsTimerActive(gpioTimer) && totalDurationTicks > 0) {
      // Calculate how many seconds are left for the timer to end
      TickType_t now = xTaskGetTickCount();
      uint64_t elapsedTicks = 0;

      if (now >= timerStartTick) {
        elapsedTicks = now - timerStartTick;
      } else {
        elapsedTicks = (0xFFFFFFFFULL - timerStartTick) + now + 1;
      }

      uint64_t remainingTicks = (totalDurationTicks > elapsedTicks) ? totalDurationTicks - elapsedTicks : 0;
      uint64_t remainingSeconds = remainingTicks / (uint64_t)configTICK_RATE_HZ;

      // Return the amount of seconds left and action that will execute when the timer ends
      String json = "{\"status\":\"Setting GPIO "+ String(ledPin) +" to " + 
                    String(targetAction ? "ON" : "OFF") + "\",";
      json += "\"active\":true,";
      json += "\"remaining_seconds\":" + String((unsigned long)remainingSeconds) + "}";
      request->send(200, "application/json", json);
    } else {
      request->send(200, "application/json", "{\"status\":\"No active timers\",\"active\":false,\"remaining_seconds\":0}");
    }
  });

  server.begin();
  Serial.println("ESP32 Web Server Timer Schedule is Ready!");
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(100));
}

View raw code

You just need to insert your network credentials in the code and it will work straight away.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_SSID";

How the Code Works

Continue reading to learn how the code works or skip to the Demonstration section.

Including Libraries

First, include the required libraries to connect to Wi-Fi, create the web server, and use FreeRTOS timers.

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <freertos/timers.h>

Network Credentials

Insert your network credentials in the following variables so that the ESP32 connects to your network.

// REPLACE WITH YOUR NETWORK CREDENTIALS
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_SSID";

LED Pin

Define the GPIO that will be used to control the LED. You can modify the ledPin variable to control any other GPIO.

const int ledPin = 5;

Creating a Server Object

Create an AsyncWebServer object called server on port 80.

AsyncWebServer server(80);

Timers

Create auxiliary variables to run the timers.

TimerHandle_t gpioTimer = NULL;
// Timer action: 1 = ON, 0 = OFF
int targetAction = -1;

TickType_t timerStartTick = 0;
uint64_t totalDurationTicks = 0;

Learn more about FreeRTOS timers: ESP32 with FreeRTOS: Software Timers/Timer Interrupts (Arduino IDE).

Web Page

The index_html variable contains the text with the HTML, CSS, and JavaScript to build the web page.

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <title>ESP32 Web Server - Timer Schedule</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin: 0; padding: 20px; background: #f4f4f4; }
    h1 { color: #333; }
    .card { max-width: 440px; margin: 20px auto; padding: 25px; background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
    button { width: 100%; padding: 14px; margin: 8px 0; font-size: 18px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
    .onButton { background: #28a745; color: white; }
    .offButton { background: #999999; color: white; }
    button:hover { opacity: 0.95; }
    .state { font-size: 22px; font-weight: bold; margin: 15px 0; }
    .ledOn { color: #28a745; }
    .ledOff { color: #999999; }
    label { display: block; margin: 12px 0 6px; font-weight: bold; text-align: left; }
    select, input[type="number"] { width: 100%; padding: 12px; margin: 8px 0; font-size: 16px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box;}
    .startButton { background:#1a73e8; color:white; }
    #cancelButton { display:none; background:#dc3545; color:white; padding:10px 20px; border:none; border-radius:6px; cursor:pointer; margin-top:10px; }
  </style>
</head>
<body>
  <h1>ESP32 - Timer Schedule</h1>
  <div class="card">
    <div class="state">GPIO is <span id="ledState">Loading...</span></div>
    <button class="onButton" onclick="controlLED(1)">TURN ON</button>
    <button class="offButton" onclick="controlLED(0)">TURN OFF</button>
  </div>
  <div class="card">
    <h2>Set Timer</h2>
    <form id="timerForm" action="/set-timer" method="POST">
      <label for="action">Action</label>
      <select name="action" id="action">
        <option value="" disabled selected hidden>--- Select Action ---</option>
        <option value="1">TURN ON (HIGH)</option>
        <option value="0">TURN OFF(LOW)</option>
      </select>
      <label for="unit">Time Unit</label>
      <select name="unit" id="unit">
        <option value="s">Seconds</option>
        <option value="m">Minutes</option>
        <option value="h">Hours</option>
      </select>
      <label for="duration">Duration</label>
      <input type="number" name="duration" id="duration" min="1" value="30" required>
      <button type="submit" class="startButton">START TIMER</button>
    </form>

    <div style="margin-top:20px; padding:15px; background:#f8f9fa; border-radius:8px;">
      <strong>Timer Status:</strong> <span id="status">No active timers</span><br><br>
      <span id="remaining" style="font-size:18px;"></span><br>
      <button id="cancelButton" onclick="cancelTimer()">
        CANCEL TIMER
      </button>
    </div>
  </div>

  <script>
    // Update LED State
    function updateLEDState() {
      fetch('/led-state')
        .then(r => r.json())
        .then(data => {
          const stateEl = document.getElementById('ledState');
          if (data.state === 1) {
            stateEl.innerHTML = '<span class="ledOn">ON</span>';
          } else {
            stateEl.innerHTML = '<span class="ledOff">OFF</span>';
          }
        })
        .catch(() => {
          document.getElementById('ledState').innerHTML = 'Error';
        });
    }
    // Control LED
    function controlLED(state) {
      fetch('/control?state=' + state)
        .then(() => updateLEDState());
    }

    // Timer functions
    let countdownInterval = null;
    let remainingSeconds = 0;
    // Format time to hours, minutes and seconds    
    function formatTime(seconds) {
      if (seconds <= 0) return "0s";
      let h = Math.floor(seconds / 3600);
      let m = Math.floor((seconds % 3600) / 60);
      let s = seconds % 60;
      if (h > 0) return h + "h " + m + "m " + s + "s";
      if (m > 0) return m + "m " + s + "s";
      return s + "s";
    }
    // Check the remaining time for the timer to end
    function updateTimerStatus() {
      fetch('/timer-status')
        .then(r => r.json())
        .then(data => {
          document.getElementById('status').innerText = data.status;

          const cancelButton = document.getElementById('cancelButton');
          const remainingEl = document.getElementById('remaining');

          if (data.active) {
            remainingSeconds = data.remaining_seconds;
            cancelButton.style.display = 'inline-block';

            if (!countdownInterval) {
              countdownInterval = setInterval(() => {
                if (remainingSeconds > 0) {
                  remainingSeconds--;
                  remainingEl.innerText = "Remaining: " + formatTime(remainingSeconds);
                } else {
                  clearInterval(countdownInterval);
                  countdownInterval = null;
                }
              }, 1000);
            }
            remainingEl.innerText = "Remaining: " + formatTime(remainingSeconds);
          } else {
            cancelButton.style.display = 'none';
            remainingEl.innerText = "";
            if (countdownInterval) {
              clearInterval(countdownInterval);
              countdownInterval = null;
            }
          }
        });
    }
    // Cancel timer button
    function cancelTimer() {
      if (confirm("Cancel the current timer?")) {
        fetch('/cancel-timer', { method: 'POST' })
          .then(() => updateTimerStatus());
      }
    }

    // Update every 10 seconds
    setInterval(() => {
      updateLEDState();
      updateTimerStatus();
    }, 10000);
    window.onload = () => {
      updateLEDState();
      updateTimerStatus();
    };
  </script>
</body>
</html>
)rawliteral";

In the web page, we create two buttons that make requests on the following URL paths to control the LED on/off:

  • TURN ON: /control?state=1
  • TURN OFF: /control?state=0
<button class="onButton" onclick="controlLED(1)">TURN ON</button>
<button class="offButton" onclick="controlLED(0)">TURN OFF</button>

The other main section of the web page is the HTML web form where you can select the timer action, unit and duration. It also shows the current state of the timer, time remaining and a button to cancel the timer.

<div class="card">
  <h2>Set Timer</h2>
  <form id="timerForm" action="/set-timer" method="POST">
    <label for="action">Action</label>
    <select name="action" id="action">
      <option value="" disabled selected hidden>--- Select Action ---</option>
      <option value="1">TURN ON (HIGH)</option>
      <option value="0">TURN OFF(LOW)</option>
    </select>
    <label for="unit">Time Unit</label>
    <select name="unit" id="unit">
      <option value="s">Seconds</option>
      <option value="m">Minutes</option>
      <option value="h">Hours</option>
    </select>
    <label for="duration">Duration</label>
    <input type="number" name="duration" id="duration" min="1" value="30" required>
    <button type="submit" class="startButton">START TIMER</button>
  </form>
  <div style="margin-top:20px; padding:15px; background:#f8f9fa; border-radius:8px;">
    <strong>Timer Status:</strong> <span id="status">No active timers</span><br><br>
    <span id="remaining" style="font-size:18px;"></span><br>
    <button id="cancelButton" onclick="cancelTimer()">
      CANCEL TIMER
    </button>
  </div>
</div>

Timer Callback Function

The timerCallback() function is the FreeRTOS one-shot timer handler that, when it expired, sets the GPIO ON/OFF based on the previously selected action in the web server.

void IRAM_ATTR timerCallback(TimerHandle_t xTimer) {
  if (targetAction != -1) {
    digitalWrite(ledPin, targetAction ? HIGH : LOW);
    Serial.printf("Timer finished: GPIO %d set to %s\n", ledPin, targetAction ? "HIGH (ON)" : "LOW (OFF)");
  }
  targetAction = -1;
  timerStartTick = 0;
  totalDurationTicks = 0;
}

setup()

In the setup(), we start by initializing the Serial Monitor and preparing the GPIO.

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

pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

Then, create the gpioTimer and assign the timerCallback function described earlier that runs when the timer expires.

gpioTimer = xTimerCreate("GPIO_Timer", pdMS_TO_TICKS(1000), pdFALSE, 0, timerCallback);

Start the Wi-Fi connection and print the ESP32 IP address in the Serial Monitor.

WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
Serial.println("\nWi-Fi Connected!");
// Print the ESP32 IP Address
Serial.print("Access ESP32 IP Address: http://");
Serial.println(WiFi.localIP());

Root URL /

Then, handle the web server. When you receive a request on the root (/) URL (this happens when you access the ESP IP address), send the HTML text stored in PROGMEM to display the web page:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send_P(200, "text/html", index_html);
});

LED Control Route /control

This GET route controls the LED ON/OFF based on the state parameter (1 = HIGH and 0 = LOW).

server.on("/control", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (request->hasParam("state")) {
    int state = request->getParam("state")->value().toInt();
    digitalWrite(ledPin, state ? HIGH : LOW);
    Serial.printf("LED set to %s\n", state ? "ON" : "OFF");
  }
  request->send(200, "text/plain", "OK");
});

GET LED State Route /led-state

This GET route reads the current state of the LED and returns it as a simple JSON object containing the state value.

server.on("/led-state", HTTP_GET, [](AsyncWebServerRequest *request) {
  int state = digitalRead(ledPin);
  request->send(200, "application/json", "{\"state\":" + String(state) + "}");
});

Set Timer Route /set-timer

This HTTP POST route receives the data from the web page with the selected action, duration, and time unit. It converts the duration into seconds and then FreeRTOS ticks to start the one-shot timer. When the timer expires, the action runs.

server.on("/set-timer", HTTP_POST, [](AsyncWebServerRequest *request) {
  if (request->hasParam("action", true) && request->hasParam("duration", true) && request->hasParam("unit", true)) {

    targetAction = request->getParam("action", true)->value().toInt();
    int dur = request->getParam("duration", true)->value().toInt();
    String unit = request->getParam("unit", true)->value();

    uint64_t durationSeconds = 0;
    if (unit == "s")      durationSeconds = (uint64_t)dur;
    else if (unit == "m") durationSeconds = (uint64_t)dur * 60ULL;
    else if (unit == "h") durationSeconds = (uint64_t)dur * 3600ULL;

    if (durationSeconds > 0) {
      if (xTimerIsTimerActive(gpioTimer)) xTimerStop(gpioTimer, 0);

      uint64_t tempTicks = durationSeconds * (uint64_t)configTICK_RATE_HZ;
      TickType_t timerTicks = (tempTicks > 0xFFFFFFFFULL) ? 0xFFFFFFFFULL : (TickType_t)tempTicks;

      timerStartTick = xTaskGetTickCount();
      totalDurationTicks = tempTicks;

      xTimerChangePeriod(gpioTimer, timerTicks, 0);
      xTimerStart(gpioTimer, 0);

      Serial.printf("Timer scheduled: GPIO %d will be set to %s in %llu seconds\n", ledPin,
                    targetAction ? "ON" : "OFF", durationSeconds);
    }
  }
  request->redirect("/");
});

Cancel Timer Route /cancel-timer

This route stops any active FreeRTOS timer and resets the timer global variables (timerStartTick, totalDurationTicks) to their idle state.

server.on("/cancel-timer", HTTP_POST, [](AsyncWebServerRequest *request) {
  // Stop the active timer
  if (xTimerIsTimerActive(gpioTimer)) {
    xTimerStop(gpioTimer, 0);
    Serial.println("Timer cancelled");
  }
  targetAction = -1;
  timerStartTick = 0; 
  totalDurationTicks = 0;
  request->send(200, "text/plain", "OK");
});

Timer Status Route /timer-status

This route checks if a timer is running, calculates the exact remaining time in seconds, and returns a JSON object with the current status message.

server.on("/timer-status", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (targetAction != -1 && xTimerIsTimerActive(gpioTimer) && totalDurationTicks > 0) {
    // Calculate how many seconds are left for the timer to end
    TickType_t now = xTaskGetTickCount();
    uint64_t elapsedTicks = 0;

    if (now >= timerStartTick) {
      elapsedTicks = now - timerStartTick;
    } else {
      elapsedTicks = (0xFFFFFFFFULL - timerStartTick) + now + 1;
    }

    uint64_t remainingTicks = (totalDurationTicks > elapsedTicks) ? totalDurationTicks - elapsedTicks : 0;
    uint64_t remainingSeconds = remainingTicks / (uint64_t)configTICK_RATE_HZ;

    // Return the amount of seconds left and action that will execute when the timer ends
    String json = "{\"status\":\"Setting GPIO "+ String(ledPin) +" to " + 
                  String(targetAction ? "ON" : "OFF") + "\",";
    json += "\"active\":true,";
    json += "\"remaining_seconds\":" + String((unsigned long)remainingSeconds) + "}";
    request->send(200, "application/json", json);
  } else {
    request->send(200, "application/json", "{\"status\":\"No active timers\",\"active\":false,\"remaining_seconds\":0}");
  }
});

Initialize the Server

Finally, call the begin() method on the server object to initialize the server.

server.begin();

Demonstration

After inserting your network credentials, upload the code to the ESP32 board. After uploading, open the Serial Monitor at a baud rate of 115200 and press the ESP32 RST button.

Open Serial Monitor Arduino 2

The ESP32 IP address will be printed in the Serial Monitor.

ESP32 Web Server Set Timer Schedule Arduino IDE Serial Monitor IP Address

With an ESP32 that has an LED connected to GPIO 5, open any browser on your local network and type the ESP32 IP address. The following web page will load.

ESP32 Web Server Set Timer Schedule Web Page

Click on the TURN ON or TURN OFF buttons to control the LED. You can also check the current LED state on the text label.

ESP32 Web Server Timer Schedule Arduino IDE Turn LED On Off Buttons

Next, you have an HTML form where you can select the action, time unit and timer duration. After selecting your desired timer settings, press the “START TIMER” button.

ESP32 Web Server Timer Schedule Arduino IDE Set Timer HTML Web Form

In the bottom of the web page, the time status will show the current pending action and remaining time. There’s also a button to cancel the timer.

ESP32 Web Server Timer Schedule Arduino IDE Time Remaining Cancel Timer Button

In the Arduino IDE Serial Monitor, you can check all the actions that were performed in the web server.

ESP32 Web Server Set Timer Schedule Arduino IDE Serial Monitor Demonstration

Wrapping Up

In this tutorial, you learned how to create a simple web server to create a timer to control your ESP32 GPIOs. You can now extend this project to control more outputs. You can take a look at other tutorials:

If you would like to learn more about building web servers with the ESP32 from scratch, we recommend taking a look at our eBook dedicated to this subject:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.