In this guide, we’ll build a web server with the ESP32 to control a DC motor remotely. The web server will serve a web page with buttons to make the motor spin forward, backward, and stop. To control the motors we’ll use the L298N motor driver and the ESP32 will be programmed using Arduino IDE.
To better understand how this project works, we recommend taking a look at our DC motor getting started guide with the ESP32:
Prerequisites
Before proceeding with the tutorial, make sure you check the following prerequisites.
1) Parts Required
To follow this tutorial, you need the following parts:
- Mini DC Motor
- L298N Motor Driver
- ESP32 (read Best ESP32 Development Boards)
- Jumper Wires
- External power supply for the motors—we’ll be using 4xAA batteries
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
2) Arduino IDE and ESP32 Boards Add-on
We’ll program the ESP32 using Arduino IDE. So, you must have the ESP32 add-on installed. Follow the next tutorial if you haven’t already:
If you want to use VS Code with the PlatformIO extension, follow the next tutorial instead to learn how to program the ESP32:
3) Install Libraries
To build this project, you need to install the following libraries:
- ESPAsyncWebServer (.zip folder)
- AsyncTCP (.zip folder)
The ESPAsyncWebServer and AsynTCP libraries aren’t available to install through the Arduino Library Manager. You need to click on the previous links to download the library files. Then, in your Arduino IDE, go to Sketch> Include Library > Add .zip Library and select the libraries you’ve just downloaded.
4) Schematic Diagram
The following schematic diagram shows how to connect a DC motor to the ESP32 using a L298N motor driver.
Not familiar with the L298N motor driver? Follow this tutorial first: ESP32 Servo Motor Web Server with Arduino IDE.
The motor we’ll control is connected to the motor A output pins, so we need to wire the ENABLEA, INPUT1, and INPUT2 pins of the motor driver to the ESP32. Follow the next schematic diagram to wire the DC motor and the L298N motor driver to the ESP32.
LN298N Motor Driver | Input 1 | Input 2 | Enable | GND |
ESP32 | GPIO 27 | GPIO 26 | GPIO 14 | GND |
We’re using the GPIOs on the previous table to connect to the motor driver. You can use any other suitable GPIOs as long as you modify the code accordingly. Learn more about the ESP32 GPIOs: ESP32 Pinout Reference Guide.
Powering the LN298N Motor Driver
The DC motor requires a big jump in current to move, so the motors should be powered using an external power source from the ESP32. As an example, we’re using 4AA batteries, but you can use any other suitable power supply. In this configuration, you can use a power supply with 6V to 12V.
The switch between the battery holder and the motor driver is optional, but it is very handy to cut and apply power. This way you don’t need to connect and then disconnect the wires to save power constantly.
We recommend soldering a 0.1uF ceramic capacitor to the positive and negative terminals of the DC motor, as shown in the diagram to help smooth out any voltage spikes. (Note: the motors also work without the capacitor.)
Project Overview
The following image shows the web page you’ll build for this project.
The web page has three buttons to control the DC motor. Each button makes a request on a different URL so that the ESP32 knows which button was pressed and acts accordingly.
- Forward button: makes a request on the /forward URL — the motor will spin forward
- Backward button: makes a request on the /backward URL—the motor will spin backward
- Stop button: makes a request on the /stop URL—the motor will stop
For simplicity, we’ll save the HTML and CSS to build the web page on a variable on the Arduino sketch.
ESP32 Web Server: Control a DC Motor – Arduino Sketch
The following code creates a web server that serves a web page with three different buttons that allows you to control a DC motor remotely via your web server.
/*********
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete instructions at https://RandomNerdTutorials.com/esp32-web-server-dc-motor-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>
// Motor A pins
int motor1Pin1 = 27;
int motor1Pin2 = 26;
int enable1Pin = 14;
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
//HTML and CSS to build the web page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<script src="https://kit.fontawesome.com/0294e3a09e.js" crossorigin="anonymous"></script>
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 50px;
}
.card-grid {
max-width: 800px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
.state {
font-size: 1.2rem;
color:#1282A2;
}
button {
border: none;
color: #FEFCFB;
padding: 15px 32px;
text-align: center;
font-size: 16px;
width: 150px;
border-radius: 4px;
transition-duration: 0.4s;
}
.button-on {
background-color:#034078;
}
.button-on:hover {
background-color: #1282A2;
}
.button-off {
background-color:#858585;
}
.button-off:hover {
background-color: #252524;
}
.button-stop {
background-color:#5e0f0f;
width: 100%;
}
.button-stop:hover {
background-color: #9b332c;
}
</style>
</head>
<body>
<div class="topnav">
<h1>CONTROL DC MOTOR</h1>
</div>
<div class="content">
<div class="card-grid">
<div class="card">
<p class="card-title"><i class="fa-solid fa-gear"></i> DC Motor A</p>
<p>
<a href="forward"><button class="button-on"><i class="fa-solid fa-arrow-up"></i> FORWARD</button></a>
<a href="backward"><button class="button-off"><i class="fa-solid fa-arrow-down"></i> BACKWARD</button></a>
</p>
<p>
<a href="stop"><button class="button-stop"><i class="fa-solid fa-stop"></i> STOP</button></a>
</p>
</div>
</div>
</div>
</body>
</html>
)rawliteral";
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
void moveForward(){
Serial.println("Moving Forward");
digitalWrite(enable1Pin, HIGH);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, HIGH);
}
void moveBackward(){
Serial.println("Moving Backward");
digitalWrite(enable1Pin, HIGH);
digitalWrite(motor1Pin1, HIGH);
digitalWrite(motor1Pin2, LOW);
}
void stopMotor(){
Serial.print("Motor Stopped");
digitalWrite(enable1Pin, LOW);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, LOW);
}
void setup() {
Serial.begin(115200);
// Set motor pins as outputs
pinMode(motor1Pin1, OUTPUT);
pinMode(motor1Pin2, OUTPUT);
pinMode(enable1Pin, OUTPUT);
initWiFi();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html);
});
server.on("/forward", HTTP_GET, [](AsyncWebServerRequest *request) {
moveForward();
request->send(200, "text/html", index_html);
});
server.on("/backward", HTTP_GET, [](AsyncWebServerRequest *request) {
moveBackward();
request->send(200, "text/html", index_html);
});
// Route to set GPIO state to LOW (inverted logic for ESP8266)
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request) {
stopMotor();
request->send(200, "text/html", index_html);
});
server.begin();
}
void loop() {
}
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 and create the web server.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
Motor Pins
Define the GPIOs that will be controlling the motor.
// Motor A pins
int motor1Pin1 = 27;
int motor1Pin2 = 26;
int enable1Pin = 14;
Network Credentials
Insert your network credentials on 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";
Creating a Server Object
Create an AsyncWebServer object called server on port 80.
AsyncWebServer server(80);
Web Page
The index_html variable contains the text with the HTML and CSS to build the web page.
//HTML and CSS to build the web page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
.topnav {
overflow: hidden;
background-color: #0A1128;
}
body {
margin: 0;
}
.content {
padding: 50px;
}
.card-grid {
max-width: 800px;
margin: 0 auto;
display: grid;
grid-gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.card {
background-color: white;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
font-size: 1.2rem;
font-weight: bold;
color: #034078
}
.state {
font-size: 1.2rem;
color:#1282A2;
}
button {
border: none;
color: #FEFCFB;
padding: 15px 32px;
text-align: center;
font-size: 16px;
width: 150px;
border-radius: 4px;
transition-duration: 0.4s;
}
.button-on {
background-color:#034078;
}
.button-on:hover {
background-color: #1282A2;
}
.button-off {
background-color:#858585;
}
.button-off:hover {
background-color: #252524;
}
.button-stop {
background-color:#5e0f0f;
width: 100%;
}
.button-stop:hover {
background-color: #9b332c;
}
</style>
</head>
<body>
<div class="topnav">
<h1>CONTROL DC MOTOR</h1>
</div>
<div class="content">
<div class="card-grid">
<div class="card">
<p class="card-title"><i class="fa-solid fa-gear"></i> DC Motor A</p>
<p>
<a href="forward"><button class="button-on"><i class="fa-solid fa-arrow-up"></i> FORWARD</button></a>
<a href="backward"><button class="button-off"><i class="fa-solid fa-arrow-down"></i> BACKWARD</button></a>
</p>
<p>
<a href="stop"><button class="button-stop"><i class="fa-solid fa-stop"></i> STOP</button></a>
</p>
</div>
</div>
</div>
</body>
</html>
)rawliteral";
Here, we create three buttons that will make a request on the following URL paths:
- FORWARD: /forward
- BACKWARD: /backward
- STOP: /stop
<p>
<a href="forward"><button class="button-on"><i class="fa-solid fa-arrow-up"></i> FORWARD</button></a>
<a href="backward"><button class="button-off"><i class="fa-solid fa-arrow-down"></i> BACKWARD</button></a>
</p>
<p>
<a href="stop"><button class="button-stop"><i class="fa-solid fa-stop"></i> STOP</button></a>
</p>
Initialize Wi-Fi
The initWiFi() function connects to wi-fi using the SSID and password you’ve defined previously.
void initWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println(WiFi.localIP());
}
Functions to Control the DC Motors
We create three functions to control the DC Motor.
The moveForward() function will move the motor forward.
void moveForward(){
Serial.println("Moving Forward");
digitalWrite(enable1Pin, HIGH);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, HIGH);
}
The moveBackward() function moves the motor backward.
void moveBackward(){
Serial.println("Moving Backward");
digitalWrite(enable1Pin, HIGH);
digitalWrite(motor1Pin1, HIGH);
digitalWrite(motor1Pin2, LOW);
}
Finally, the stopMotor() function stops the motor by setting all motor pins to LOW.
void stopMotor(){
Serial.print("Motor Stopped")
digitalWrite(enable1Pin, LOW);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, LOW);
}
setup()
In the setup(), initialize the Serial Monitor for debugging purposes.
Serial.begin(115200);
Set the motor pins as outputs.
// Set motor pins as outputs
pinMode(motor1Pin1, OUTPUT);
pinMode(motor1Pin2, OUTPUT);
pinMode(enable1Pin, OUTPUT);
Initialize Wi-Fi by calling the initWiFi() function.
initWiFi();
Handle Requests
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 to build the web page:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html);
});
When you click the Forward button, you make a request on the /forward path. When the ESP32 receives that request, it will call the moveForward() function to make the motor move forward.
server.on("/forward", HTTP_GET, [](AsyncWebServerRequest *request) {
moveForward();
request->send(200, "text/html", index_html);
});
Similarly, when you click the Backward button, the ESP32 receives a request on the /backward path. When that happens, call the moveBackward() function to make the motor move in the other direction.
server.on("/backward", HTTP_GET, [](AsyncWebServerRequest *request) {
moveBackward();
request->send(200, "text/html", index_html);
});
When you click the Stop button, the ESP32 receives a request on the /stop path and calls the stopMotor() function.
// Route to set GPIO state to LOW (inverted logic for ESP8266)
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request) {
stopMotor();
request->send(200, "text/html", index_html);
});
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.
The ESP32 IP address will be printed in the Serial Monitor.
With the motor connected to the ESP32 via the L298N motor driver and powered up with an external power supply, open any browser on your local network and type the ESP32 IP address. The following web page will load.
Click on the buttons to control the DC motor.
The server can handle multiple clients simultaneously. You can also control the DC motor from your smartphone as long as it is connected to your local network.
Wrapping Up
In this tutorial, you’ve learned how to create a simple asynchronous web server to control a DC motor remotely from your computer or smartphone. You can now extend this project to control more motors or control other outputs or even display sensor readings. You can take a look at the following tutorials:
- ESP32 Web Server: Display Sensor Readings in Gauges
- ESP32 Web Server: Control Stepper Motor (WebSocket)
- ESP32 Web Server (WebSocket) with Multiple Sliders: Control LEDs Brightness (PWM)
- ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically)
If you would like to learn more about building web servers with the ESP32 from scratch, we recommend taking a look at our exclusive eBook:
Thanks for reading.
It works great – thank you very much
You’re welcome 😀
It works well with L298N up to 12v but then there is a problem with the BTS7960 the 3.3v outputs are too weak and you need CrusS0108E it seems
Hi,
My problem is with AsyncWebServer library. Have tried lots of suggestions
off the web, no luck. here is the results readout.
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\WebAuthentication.cpp: In function ‘bool getMD5(uint8_t*, uint16_t, char)’:
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\WebAuthentication.cpp:74:3: error: ‘mbedtls_md5_starts_ret’ was not declared in this scope; did you mean ‘mbedtls_md5_starts’?
74 | mbedtls_md5_starts_ret(&_ctx);
| ^~~~~~~~~~~~~~~~~~~~~~
| mbedtls_md5_starts
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\WebAuthentication.cpp:75:3: error: ‘mbedtls_md5_update_ret’ was not declared in this scope; did you mean ‘mbedtls_md5_update’?
75 | mbedtls_md5_update_ret(&_ctx, data, len);
| ^~~~~~~~~~~~~~~~~~~~~~
| mbedtls_md5_update
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\WebAuthentication.cpp:76:3: error: ‘mbedtls_md5_finish_ret’ was not declared in this scope; did you mean ‘mbedtls_md5_finish’?
76 | mbedtls_md5_finish_ret(&_ctx, _buf);
| ^~~~~~~~~~~~~~~~~~~~~~
| mbedtls_md5_finish
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\AsyncEventSource.cpp: In member function ‘void AsyncEventSourceClient::_queueMessage(AsyncEventSourceMessage)’:
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\AsyncEventSource.cpp:189:7: error: ‘ets_printf’ was not declared in this scope; did you mean ‘vswprintf’?
189 | ets_printf(“ERROR: Too many messages queued\n”);
| ^~~~~~~~~~
| vswprintf
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\AsyncWebSocket.cpp: In member function ‘void AsyncWebSocketClient::_queueMessage(AsyncWebSocketMessage*)’:
C:\Users\Ross\Documents\Arduino\libraries\ESPAsyncWebServer\src\AsyncWebSocket.cpp:549:7: error: ‘ets_printf’ was not declared in this scope; did you mean ‘vswprintf’?
549 | ets_printf(“ERROR: Too many messages queued\n”);
| ^~~~~~~~~~
| vswprintf
exit status 1
Compilation error: exit status 1
Hi.
Make sure you install the libraries as we mention in the tutorial.
Delete any libraries with the same name you may have installed previously.
Once you have installed the libraries via .ZIP file, update them to the latest version in the Arduino IDe.
Regards,
Sara
Great tutorial and works perfectly for my actuator! However, I’ve been trying to add a delay of 30 seconds after selecting forward/backward movement, so it disables the outputs after the time period (essentially sending the stopMotor command automatically):
void moveForward(){
Serial.println(“Moving Forward”);
digitalWrite(enable1Pin, HIGH);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, HIGH);
delay(30000);
digitalWrite(enable1Pin, LOW);
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, LOW);
}
It does not seem to change the pin states after the time period though.