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.
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.
- 1 × ESP32 DevKit V1
- 1 × Pressure, humidity and temperature, BME280
- 1 × Power, 3.3 V
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, yellowIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × Relay moduleIn Halloween Prop Kit.
- 1 × Power, 5 V
- 1 × DC jack and adapter
- 1 × Filament lamp
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 pin | Connects to |
|---|---|
| Resistor, 220 Ω pin 2 | ESP32 DevKit V1 pin D26 |
| Relay module pin IN | ESP32 DevKit V1 pin D27 |
| Pressure, humidity and temperature, BME280 pin SCL | ESP32 DevKit V1 pin D22 |
| Pressure, humidity and temperature, BME280 pin SDA | ESP32 DevKit V1 pin D21 |
| Pressure, humidity and temperature, BME280 pin VIN | 3.3 V |
| Pressure, humidity and temperature, BME280 pin GND | Ground |
| LED, yellow pin A | Resistor, 220 Ω pin 1 |
| LED, yellow pin C | Ground |
| Relay module pin GND | Ground |
| Relay module pin VCC | 5 V |
| Filament lamp pin 1 | Relay module pin NO |
| DC jack and adapter pin V+ | Relay module pin COM |
| DC jack and adapter pin V- | Ground |
| Filament lamp pin 2 | Ground |
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.
/*
* 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% °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 editorloop 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 inbme.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.