Built-in project · ESP32 DevKit V1

ESP32 async web server: BME280, slider and relay

ESPAsyncWebServer serves a page with BME280 temperature, humidity and pressure, a slider that dims an LED and a button that switches a relay. The whole build, an ESP32 DevKit and 6 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $21. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Serving a web page from an ESP32 with ESPAsyncWebServer
  • Filling a page template with live sensor values
  • Dimming an LED with ESP32 PWM
  • Switching a mains-style load safely with a relay module
  • Reading a BME280 over I2C
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: ESP32 DevKit V1
  • Parts: 9

How it works

The ESP32 joins WiFi, prints its address on the Serial Monitor, and starts a web server on port 80. A BME280 on the I2C pins (SDA on D21, SCL on D22) supplies temperature, humidity and pressure. An LED on D26 is dimmed with PWM, and a relay on D27 switches a 12 V lamp.

The web page is a template with markers like %TEMPERATURE%. As the server sends the page, processor fills each marker with a fresh value, so each load shows the newest readings.

The slider and relay button are plain HTML forms. Each one calls its own address (/slider or /relay), the ESP32 changes the LED or relay, and the browser is sent back to the page. loop is empty because the server works in the background.

Parts list

9 parts, plus the jumper wires. They retail for about $21; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Resistor, 220 Ω pin 2ESP32 DevKit V1 pin D26
Relay module pin INESP32 DevKit V1 pin D27
Pressure, humidity and temperature, BME280 pin SCLESP32 DevKit V1 pin D22
Pressure, humidity and temperature, BME280 pin SDAESP32 DevKit V1 pin D21
Pressure, humidity and temperature, BME280 pin VIN3.3 V
Pressure, humidity and temperature, BME280 pin GNDGround
LED, yellow pin AResistor, 220 Ω pin 1
LED, yellow pin CGround
Relay module pin GNDGround
Relay module pin VCC5 V
Filament lamp pin 1Relay module pin NO
DC jack and adapter pin V+Relay module pin COM
DC jack and adapter pin V-Ground
Filament lamp pin 2Ground

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run and wait for "Open http://..." on the Serial Monitor.
2. Open the Web panel: the page shows the BME280's temperature, humidity and pressure.
3. Move the slider and press Set: the LED dims or brightens.
4. Press Turn on: the relay clicks and the 12 V lamp lights.
5. Change the BME280's sliders and press Refresh readings.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · ESP32 DevKit V1
/*
 * ESP32 async web server: a page with a BME280's readings, a slider that
 * sets an LED's brightness and a button that switches a relay, served by
 * ESPAsyncWebServer with nothing in loop().
 *
 *   BME280   VIN -> 3V3, GND -> GND, SDA -> GPIO 21 (D21), SCL -> GPIO 22 (D22)
 *   LED      GPIO 26 (D26) -> 220 ohm -> LED -> GND, dimmed with ledcWrite()
 *   Relay    module IN -> GPIO 27 (D27), VCC -> 5 V, GND -> GND
 *            (a 5 V module that switches on a 3.3 V high; COM and NO switch
 *            the 12 V lamp)
 *
 * Libraries: ESPAsyncWebServer and AsyncTCP (ESP32Async), Adafruit BME280.
 *
 * The page is a template: the server fills each %NAME% from processor()
 * as it sends it, so every load shows the newest readings. The slider and
 * the relay button are plain HTML forms, so they work in any browser
 * without JavaScript: /slider?value=128 and /relay?state=on answer by
 * sending the browser back to the page.
 *
 * Press Run, wait for the IP on the Serial Monitor, then open the Web panel.
 * In Mokxi the network is simulated and the Web panel is the only browser;
 * on a real ESP32 any phone on the same WiFi opens http://<the IP>/.
 */
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

const char *SSID = "Mokxi-Lab";
const char *PASSWORD = "breadboard";

const int LED_PIN = 26;
const int RELAY_PIN = 27;

Adafruit_BME280 bme;
AsyncWebServer server(80);

int brightness = 0; // 0 to 255
bool relayOn = false;

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32 Async Web Server</title>
<style>body{font-family:sans-serif;text-align:center}td{padding:4px 12px}</style>
</head><body>
<h2>ESP32 Async Web Server</h2>
<table align="center">
<tr><td>Temperature</td><td>%TEMPERATURE% &deg;C</td></tr>
<tr><td>Humidity</td><td>%HUMIDITY% %</td></tr>
<tr><td>Pressure</td><td>%PRESSURE% hPa</td></tr>
</table>
<h3>LED brightness: %BRIGHTNESS%</h3>
<form action="/slider">
<input type="range" name="value" min="0" max="255" value="%BRIGHTNESS%">
<input type="submit" value="Set">
</form>
<h3>Relay: %RELAYSTATE%</h3>
<form action="/relay"><button name="state" value="%RELAYNEXT%">Turn %RELAYNEXT%</button></form>
<p><a href="/">Refresh readings</a></p>
</body></html>
)rawliteral";

// Fills each %PLACEHOLDER% of the page.
String processor(const String &var)
{
    if (var == "TEMPERATURE") return String(bme.readTemperature(), 1);
    if (var == "HUMIDITY") return String(bme.readHumidity(), 1);
    if (var == "PRESSURE") return String(bme.readPressure() / 100.0F, 1);
    if (var == "BRIGHTNESS") return String(brightness);
    if (var == "RELAYSTATE") return relayOn ? "on" : "off";
    if (var == "RELAYNEXT") return relayOn ? "off" : "on";
    return String();
}

void setup()
{
    Serial.begin(115200);
    ledcAttach(LED_PIN, 5000, 8); // 5 kHz PWM, 8 bits: duty 0 to 255
    ledcWrite(LED_PIN, brightness);
    pinMode(RELAY_PIN, OUTPUT);
    digitalWrite(RELAY_PIN, LOW);

    if (!bme.begin(0x76)) {
        Serial.println("No BME280 at 0x76: check SDA, SCL and the address");
    }

    WiFi.begin(SSID, PASSWORD);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println();
    Serial.print("Open http://");
    Serial.println(WiFi.localIP());

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

    // /slider?value=0..255 sets the LED's brightness
    server.on("/slider", HTTP_GET, [](AsyncWebServerRequest *request) {
        if (request->hasParam("value")) {
            brightness = constrain(request->getParam("value")->value().toInt(), 0, 255);
            ledcWrite(LED_PIN, brightness);
            Serial.print("LED brightness ");
            Serial.println(brightness);
        }
        request->redirect("/");
    });

    // /relay?state=on or off switches the relay
    server.on("/relay", HTTP_GET, [](AsyncWebServerRequest *request) {
        if (request->hasParam("state")) {
            relayOn = request->getParam("state")->value() == "on";
            digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);
            Serial.println(relayOn ? "Relay on" : "Relay off");
        }
        request->redirect("/");
    });

    // The readings as JSON, for another program to read
    server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request) {
        String json = "{\"temperature\":" + String(bme.readTemperature(), 1) +
                      ",\"humidity\":" + String(bme.readHumidity(), 1) +
                      ",\"pressure\":" + String(bme.readPressure() / 100.0F, 1) + "}";
        request->send(200, "application/json", json);
    });

    server.onNotFound([](AsyncWebServerRequest *request) {
        request->send(404, "text/plain", "Not found");
    });

    server.begin();
}

void loop()
{
    // Nothing here: AsyncWebServer answers requests in the background.
}

Code walkthrough

The key functions of the sketch, and what each one does.

processor sketch.ino

The server calls this once for each %NAME% marker in the page. It returns the text to put there: the BME280 readings as numbers with one decimal, the LED brightness, or the relay state. Pressure is divided by 100 to turn pascals into hPa. The relay button's label uses RELAYNEXT, the opposite of the current state.

// Fills each %PLACEHOLDER% of the page.
String processor(const String &var)
{
    if (var == "TEMPERATURE") return String(bme.readTemperature(), 1);
    if (var == "HUMIDITY") return String(bme.readHumidity(), 1);
    if (var == "PRESSURE") return String(bme.readPressure() / 100.0F, 1);
    if (var == "BRIGHTNESS") return String(brightness);
    if (var == "RELAYSTATE") return relayOn ? "on" : "off";
    if (var == "RELAYNEXT") return relayOn ? "off" : "on";
    return String();
}

setup sketch.ino

This sets up PWM for the LED with ledcAttach (5 kHz, 8 bits), sets the relay pin LOW, and starts the BME280 at address 0x76. It then connects to WiFi and prints the address. Last, it registers the routes /, /slider, /relay and /readings, and calls server.begin.

void setup()
{
    Serial.begin(115200);
    ledcAttach(LED_PIN, 5000, 8); // 5 kHz PWM, 8 bits: duty 0 to 255
    ledcWrite(LED_PIN, brightness);
    pinMode(RELAY_PIN, OUTPUT);
    digitalWrite(RELAY_PIN, LOW);

    if (!bme.begin(0x76)) {
        Serial.println("No BME280 at 0x76: check SDA, SCL and the address");
    }

    WiFi.begin(SSID, PASSWORD);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println();
    Serial.print("Open http://");
    Serial.println(WiFi.localIP());

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

    // /slider?value=0..255 sets the LED's brightness
    server.on("/slider", HTTP_GET, [](AsyncWebServerRequest *request) {
  // ... 33 more lines in the editor

loop sketch.ino

It is empty on purpose. The async server answers requests in the background, so nothing needs to run here.

void loop()
{
    // Nothing here: AsyncWebServer answers requests in the background.
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The Serial Monitor says No BME280 at 0x76 This project
    Check SDA on D21 and SCL on D22. Also check that the sensor's address matches the one in bme.begin.
  • The relay clicks but the lamp does not light This project
    The lamp should go between NO and the supply, with the supply's V+ on COM and its ground on the lamp's other pin. Check those three wires.
  • The page never loads This project
    Wait for the Open http:// line on the Serial Monitor before opening the Web panel. The ESP32 prints it only after WiFi connects.
  • The sketch cannot find the sensor, or reads zeros. Pressure
    The I2C address is 0x76 when SDO is tied to GND and 0x77 when tied to VCC. Run an I2C scanner, and make sure the library address matches. Wire SDA and SCL correctly.
  • Humidity always reads zero or looks wrong. Pressure
    A BMP280 looks the same but has no humidity sensor. Check the chip ID register: 0x60 is a BME280 and 0x58 is a BMP280. Use the right library for yours.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.

Questions and answers

What makes it async?

ESPAsyncWebServer answers requests in the background, so loop() is empty and there is no handleClient() call to forget. A slow loop() does not delay the page.

How do the %TEMPERATURE% placeholders work?

The page is a template: as it is sent, the server calls processor() for each %NAME% and puts in what it returns, so every load shows the newest readings.

Why are the slider and the button forms rather than JavaScript?

Forms work in any browser with no script, and Mokxi's Web panel runs no scripts. Each form sends a GET such as /slider?value=128 and is redirected back to the page. On a real ESP32 you can use fetch() as well.

Can a 3.3 V ESP32 drive a 5 V relay module?

Most opto-isolated 5 V modules switch on from a 3.3 V high on IN. Power the module's VCC from 5 V (VIN), not from the 3V3 pin.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.