ESP8266 NodeMCU Web Server: Control Stepper Motor (WebSocket)

In this guide, you’ll learn how to create a web server with the ESP8266 NodeMCU board that displays a web page to control a stepper motor. The web page allows you to insert the number of steps and select clockwise or counterclockwise directions. Additionally, it also shows whether the motor is currently spinning or if it is stopped. The communication between the client and the server is achieved via WebSocket protocol. All clients are updated with the current motor state.

ESP8266 NodeMCU Web Server Control Stepper Motor WebSocket Arduino IDE

To better understand how this project works, you can take a look at the following tutorials:

Table of Contents

  1. Prerequisites
  2. Project Overview
  3. Organizing your files:
    1. HTML File
    2. CSS File
    3. JavaScript File
    4. Arduino Sketch
  4. Upload Code and Files
  5. Demonstration

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:

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 ESP8266 Boards Add-on

We’ll program the ESP8266 using Arduino IDE. So, you must have the ESP8266 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 ESP8266:

3) Filesystem Uploader Plugin

To upload the HTML, CSS, and JavaScript files needed to build this project to the ESP8266 filesystem (LittleFS), we’ll use a plugin for Arduino IDE: LittleFS Filesystem uploader. Follow the next tutorial to install the filesystem uploader plugin if you haven’t already:

If you’re using VS Code with the PlatformIO extension, read the following tutorial to learn how to upload files to the filesystem:

4) Libraries

There are different ways to control stepper motors with a microcontroller. To control the stepper motor with the ESP8266, we’ll use the AccelStepper library. This library allows you to easily move the motor a defined number of steps, set its speed, acceleration, and much more. The library has great documentation explaining how to use its methods. You can check it here.

Follow the next steps to install the library in your Arduino IDE.

  1. Go to Sketch > Include Library > Manage Libraries…
  2. Search for “accelstepper”.
  3. Install the AccelStepper library by Mike McCauley. We’re using version 1.61.0.
Install AccelStepper Library Arduino IDE

To build the web server, you need to install the following libraries:

The ESPAsyncWebServer and ESPAsynTCP 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 SketchInclude Library > Add .zip Library and select the libraries you’ve just downloaded.

Installing Libraries (VS Code + PlatformIO)

If you’re programming the ESP8266 using PlatformIO, you should add the following lines to the platformio.ini file to include the libraries (also change the Serial Monitor speed to 115200 and set the filesystem to LittleFS):

monitor_speed = 115200
board_build.filesystem = littlefs
lib_deps = ESP Async WebServer
    waspinator/AccelStepper @ ^1.61

5) Schematic Diagram

The following schematic diagram shows the connections between the stepper motor and the ESP8266.

ESP8266 with Stepper Motor 28BYJ-48 and ULN2003A Schematic Diagram Wiring

Note: You should power the ULN2003 motor driver using an external 5V power supply.

Motor DriverESP8266
IN1GPIO 5
IN2GPIO 4
IN3GPIO 14
IN4GPIO 12

Project Overview

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

ESP8266 NodeMCU Control Stepper Motor WebSocket Project overview
  • The web page shows a form where you can enter the number of steps you want the motor to move and select the direction: clockwise or counterclockwise.
  • It also shows the motor state: motor spinning or motor stopped. Additionally, there’s a gear icon that spins as long as the motor is spinning. The gear spins clockwise or counterclockwise direction accordingly to the chosen direction.
ESP32 ESP8266 Stepper Motor Web Server Websocket How it Works
  • The server and the client communicate using WebSocket protocol.
  • When you click on the GO! button, it calls a Javascript function that sends a message via WebSocket protocol with all the information: steps and direction (3). The message is in the following format:
steps&direction

So, if you submit 2000 steps and clockwise direction, it will send the following message:

2000&CW
  • At the same time, it will change the motor state on the web page, and the gear will start spinning in the proper direction (2).
  • Then, the server receives the message (4) and spins the motor accordingly (5).
  • When the motor stops spinning (6), the ESP will send a message to the client(s), also via WebSocket protocol, informing that the motor has stopped (7).
  • The client(s) receive this request and update the motor state on the web page (8).

Organizing your Files

The files you want to upload to the ESP filesystem should be placed in a folder called data under the project folder. We’ll move three files to that folder:

  • index.html to build the web page;
  • style.css to style the web page;
  • script.js to handle websocket communication and start/stop the gear animation.
ESP8266 organizing your Files arduino sketch index html style css script js

You should save the HTML, CSS, and JavaScript files inside a folder called data inside the Arduino sketch folder, as shown in the previous diagram. We’ll upload these files to the ESP8266 filesystem (LittleFS).

You can download all project files:

HTML File

Create a file called index.html with the following content:

<!DOCTYPE html>
<html>
<head>
  <title>Stepper Motor</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://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>
  <div class="topnav">
    <h1>Stepper Motor Control <i class="fas fa-cogs"></i></h1>
  </div>
  <div class="content">
        <form>
          <input type="radio" id="CW" name="direction" value="CW" checked>
          <label for="CW">Clockwise</label>
          <input type="radio" id="CCW" name="direction" value="CCW">
          <label for="CW">Counterclockwise</label><br><br><br>
          <label for="steps">Number of steps:</label>
          <input type="number" id="steps" name="steps">
        </form>
        <button onclick="submitForm()">GO!</button>
        <p>Motor state: <span id="motor-state">Stopped</span></p>
        <p><i id="gear" class="fas fa-cog"></i> </p>

  </div>
</body>
<script src="script.js"></script>
</html>

View raw code

This HTML file is very similar to the one used in this tutorial. You can click here for a complete explanation of the HTML file.

We’ve added ids to the HTML elements we want to manipulate using JavaScript—the radio buttons and the input field:

  • clockwise radio button: id=”CW”
  • counterclowise radio button: id=”CCW”
  • steps input field: id=”steps”
<input type="radio" id="CW" name="direction" value="CW" checked>
<label for="CW">Clockwise</label>
<input type="radio" id="CCW" name="direction" value="CCW">
<label for="CW">Counterclockwise</label><br><br><br>
<label for="steps">Number of steps:</label>
<input type="number" id="steps" name="steps">

We want to send the form results to the server (ESP8266) via WebSocket protocol. So, we’ve added a button, that when clicked (onclick event) calls the submitForm() user-defined javascript function that sends the results to the server as you’ll see later in the JavaScript section.

<button onclick="submitForm()">GO!</button>

Additionally, we also added a paragraph to display the motor state. We’ve added a <span> tag with the motor-state id so that we’re able to manipulate the text between the <span> tags using Javascript.

<p>Motor state: <span id="motor-state">Stopped</span></p>

Finally, there’s a paragraph displaying a gear with the id=”gear”. We need this id to make the gear move.

<p><i id="gear" class="fas fa-cog"></i> </p>

Don’t forget that you need to reference the JavaScript file (scrip.js) in the HTML file as follows:

<script src="script.js"></script>

CSS File

Create a file called style.css with the following content:

html {
  font-family: Arial, Helvetica, sans-serif;
}

h1 {
  font-size: 1.8rem;
  color: white;
}

p{
  font-size: 20px;
  text-align: center;
}

.topnav {
  overflow: hidden;
  background-color: #0A1128;
  text-align: center;
}

body {
  margin: 0;
}

.content {
  padding: 20px;
  max-width: max-content;
  margin: 0 auto;
}

input[type=number], select {
  width: 100%;
  padding: 12px 20px;
  margin: 8px 0;
  display: inline-block;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

form{
  border-radius: 5px;
  background-color: #f2f2f2;
  padding: 20px;
}

button {
  background-color: #034078;
  border: none;
  padding: 14px 20px;
  text-align: center;
  font-size: 20px;
  border-radius: 4px;
  transition-duration: 0.4s;
  width: 100%;
  color: white;
  cursor: pointer;
}

button:hover {
    background-color: #1282A2;
}

input[type="radio"] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border-radius: 50%;
  width: 16px;
  height: 16px;
  border: 2px solid #999;
  transition: 0.2s all linear;
  margin-right: 5px;
  position: relative;
  top: 4px;
  }

input[type="radio"]:checked{
  border: 6px solid #1282A2;
}

#motor-state{
  font-weight: bold;
  color: red;
}

#gear{
  font-size:100px;
  color:#2d3031cb;
}

.spin {
  -webkit-animation:spin 4s linear infinite;
  -moz-animation:spin 4s linear infinite;
  animation:spin 4s linear infinite;
}

.spin-back {
  -webkit-animation:spin-back 4s linear infinite;
  -moz-animation:spin-back 4s linear infinite;
  animation:spin-back 4s linear infinite;
}

@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }

@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }

View raw code

We already covered how the CSS for the HTML form works. You can click here for a detailed explanation. Let’s take a look at the relevant parts for this tutorial.

We format the motor state text font-weight (bold) and color (red). To refer to a specific id in CSS, use # followed by the id (#motor-state).

#motor-state{
  font-weight: bold;
  color: red;
}

The following lines format the gear icon color and size—remember that its id is gear, so we refer to it with #gear:

#gear{
  font-size:100px;
  color:#2d3031cb;
}

Then, we format two classes spin and spin-back that are not attributed to any HTML element yet. We’ll attribute the spin and spin-back classes to the gear using JavaScript when the motor starts moving.

These classes use the animation property to rotate the gear. To learn more about how the animation property works, we recommend taking a look at this quick tutorial.

.spin {
  -webkit-animation:spin 4s linear infinite;
  -moz-animation:spin 4s linear infinite;
  animation:spin 4s linear infinite;
}

.spin-back {
  -webkit-animation:spin-back 4s linear infinite;
  -moz-animation:spin-back 4s linear infinite;
  animation:spin-back 4s linear infinite;
}

@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }

@-moz-keyframes spin-back { 100% { -moz-transform: rotate(-360deg); } }
@-webkit-keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); } }
@keyframes spin-back { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }

JavaScript File

Create a file called script.js with the following content:

var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onload);
var direction;

function onload(event) {
    initWebSocket();
}

function initWebSocket() {
    console.log('Trying to open a WebSocket connection…');
    websocket = new WebSocket(gateway);
    websocket.onopen = onOpen;
    websocket.onclose = onClose;
    websocket.onmessage = onMessage;
}

function onOpen(event) {
    console.log('Connection opened');
}

function onClose(event) {
    console.log('Connection closed');
    document.getElementById("motor-state").innerHTML = "motor stopped"
    setTimeout(initWebSocket, 2000);
}

function submitForm(){
    const rbs = document.querySelectorAll('input[name="direction"]');
    direction;
    for (const rb of rbs) {
        if (rb.checked) {
            direction = rb.value;
            break;
        }
    }

    document.getElementById("motor-state").innerHTML = "motor spinning...";
    document.getElementById("motor-state").style.color = "blue";
    if (direction=="CW"){
        document.getElementById("gear").classList.add("spin");
    }
    else{
        document.getElementById("gear").classList.add("spin-back");
    }
    
    var steps = document.getElementById("steps").value;
    websocket.send(steps+"&"+direction);
}

function onMessage(event) {
    console.log(event.data);
    direction = event.data;
    if (direction=="stop"){ 
      document.getElementById("motor-state").innerHTML = "motor stopped"
      document.getElementById("motor-state").style.color = "red";
      document.getElementById("gear").classList.remove("spin", "spin-back");
    }
    else if(direction=="CW" || direction=="CCW"){
        document.getElementById("motor-state").innerHTML = "motor spinning...";
        document.getElementById("motor-state").style.color = "blue";
        if (direction=="CW"){
            document.getElementById("gear").classList.add("spin");
        }
        else{
            document.getElementById("gear").classList.add("spin-back");
        }
    }
}

View raw code

Let’s see how the JavaScript for this project works.

The gateway is the entry point to the WebSocket interface. window.location.hostname gets the current page address (the web server IP address)

var gateway = `ws://${window.location.hostname}/ws`;

Create a new global variable called websocket.

var websocket;

Create another global variable called direction that will hold the motor’s current direction: clockwise, counterclowise or stopped.

var direction;

Add an event listener that will call the onload function when the web page loads.

window.addEventListener('load',  onload);

The onload() function calls the initWebSocket() function to initialize a WebSocket connection with the server.

function onload(event) {
  initWebSocket();
}

The initWebSocket() function initializes a WebSocket connection on the gateway defined earlier. We also assign several callback functions that will be triggered when the WebSocket connection is opened, closed or when a message is received.

function initWebSocket() {
  console.log('Trying to open a WebSocket connection…');
  websocket = new WebSocket(gateway);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}

When the connection is opened, print a message in the console for debugging purposes.

function onOpen(event) {
  console.log('Connection opened');
}

If for some reason the web socket connection is closed, call the initWebSocket() function again after 2000 milliseconds (2 seconds).

function onClose(event) {
  console.log('Connection closed');
  setTimeout(initWebSocket, 2000);
}

Finally, we need to handle what happens when the form is submitted and when the client receives a new message (onMessage event).

When the form is submitted, the submitForm() function is called:

function submitForm(){

We start by getting which radio button is selected. We save the value of the selected radio button in the direction variable.

const rbs = document.querySelectorAll('input[name="direction"]');
var direction;
for (const rb of rbs) {
  if (rb.checked) {
    direction = rb.value;
    break;
  }
}

Then, we change the motor state text to motor spinning… and its color to blue. We refer to that HTML element by its id motor-state.

document.getElementById("motor-state").innerHTML = "motor spinning...";
document.getElementById("motor-state").style.color = "blue";

Then, we check whether we’ve selected clockwise or counterclockwise direction to spin the gear in the right direction. To do that, we add the class spin or spin-back to the element with the gear id.

if (direction=="CW"){
  document.getElementById("gear").classList.add("spin");
}
else{
  document.getElementById("gear").classList.add("spin-back");
}

We get the number of steps inserted and save it in the steps variable.

var steps = document.getElementById("steps").value;

Then, we finally send a message via WebSocket protocol to the server (ESP8266) with the number of steps and direction separated by a &.

websocket.send(steps+"&"+direction);

The server (your ESP board) will send a message when it is time to change the motor state. When that happens, we save the message in the direction variable.

We check the content of the message and change the motor state and gear animation accordingly.

function onMessage(event) {
  console.log(event.data);
  direction = event.data;
  if (direction=="stop"){ 
    document.getElementById("motor-state").innerHTML = "motor stopped"
    document.getElementById("motor-state").style.color = "red";
    document.getElementById("gear").classList.remove("spin", "spin-back");
  }
  else if(direction=="CW" || direction=="CCW"){
    document.getElementById("motor-state").innerHTML = "motor spinning...";
    document.getElementById("motor-state").style.color = "blue";
    if (direction=="CW"){
      document.getElementById("gear").classList.add("spin");
    }
    else{
      document.getElementById("gear").classList.add("spin-back");
    }
  }
}

Arduino Sketch

Before uploading, you can use the following link to:

Copy the following code to the Arduino IDE. Insert your network credentials and it will work straight away.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/stepper-motor-esp8266-websocket/
  
  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 <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include "LittleFS.h"
#include <AccelStepper.h>

#define IN1 5
#define IN2 4
#define IN3 14
#define IN4 12
AccelStepper stepper(AccelStepper::HALF4WIRE, IN1, IN3, IN2, IN4);

String message = "";

// 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);

// Create a WebSocket object
AsyncWebSocket ws("/ws");

//Variables to save values from HTML form
String direction ="STOP";
String steps;

bool notifyStop = false;

// Initialize LittleFS
void initFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  else{
    Serial.println("LittleFS mounted successfully");
  }
}

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

void notifyClients(String state) {
  ws.textAll(state);
}

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    message = (char*)data;
    steps = message.substring(0, message.indexOf("&"));
    direction = message.substring(message.indexOf("&")+1, message.length());
    Serial.print("steps");
    Serial.println(steps);
    Serial.print("direction");
    Serial.println(direction);
    notifyClients(direction);
    notifyStop = true;
    if (direction == "CW"){
      Serial.print("CW");
      stepper.move(steps.toInt());
    }
    else{
      Serial.print("CCW");
      stepper.move(-steps.toInt());
    }
  }
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      //Notify client of motor current state when it first connects
      notifyClients(direction);
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
        handleWebSocketMessage(arg, data, len);
        break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
     break;
  }
}

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

void setup() {
  // Serial port for debugging purposes

  Serial.begin(115200);
  initWiFi();
  initWebSocket();
  initFS();
  stepper.setMaxSpeed(1000);
  stepper.setAcceleration(100);

  // Web Server Root URL
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", "text/html");
  });
  
  server.serveStatic("/", LittleFS, "/");

  server.begin();
}

void loop() {
  if (stepper.distanceToGo() == 0 && notifyStop == true){  
    direction = "stop";
    notifyClients(direction);
    notifyStop = false;
  }
  ws.cleanupClients();
  stepper.run();
}

View raw code

The Arduino sketch is very similar to this tutorial, but it handles the client-server communication using WebSocket protocol. Let’s see how it works or skip to the demonstration section.

Include Libraries

First, include the required libraries. The ESP8266WiFi, ESPAsyncTCP, and ESPAsyncWebServer to create the web server, the LittleFS library to use the ESP8266 filesystem, and the AccelStepper library to control the stepper motor.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include "LittleFS.h"
#include <AccelStepper.h>

Stepper Motor Pins

Define the motor input pins. In this example, we’re connecting to GPIOs 5, 4, 14, and 12, but you can use any other suitable GPIOs.

#define IN1 5
#define IN2 4
#define IN3 14
#define IN4 12

Initialize an instance of the AccelStepper library called stepper. Pass as arguments: AccelStepper::HALF4WIRE to indicate we’re controlling the stepper motor with four wires, and the input pins. In the case of the 28BYJ-48 stepper motor, the order of the pins is IN1, IN3, IN2, IN4—it might be different for your motor.

Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);

Network Credentials

Insert your network credentials in the following lines.

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

AsyncWebServer and AsyncWebSocket

Create an AsyncWebServer object called server on port 80.

AsyncWebServer server(80);

The ESPAsyncWebServer library includes a WebSocket plugin that makes it easy to handle WebSocket connections. Create an AsyncWebSocket object called ws to handle the connections on the /ws path.

AsyncWebSocket ws("/ws");

Initializing Variables

The following variables will save the direction and number of steps received via WebSocket protocol. When the program first starts, the motor is stopped.

String direction ="stop";
String steps;

The notifyStop variable will be used to check whether the motor has reached its desired position and notify all clients that the motor is stopped.

bool notifyStop = false;

initFS()

The initFS() function initializes the ESP8266 filesystem (littleFS).

// Initialize LittleFS
void initFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  else{
    Serial.println("LittleFS mounted successfully");
  }
}

initWiFi()

The initWiFi() function initializes WiFi.

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

Handling WebSockets – Server

Previously, you’ve seen how to handle the WebSocket connection on the client side (browser). Now, let’s take a look on how to handle it on the server side.

Notify All Clients

The notifyClients() function notifies all clients with a message containing whatever you pass as a argument. In this case, we’ll want to notify all clients of the current motor state whenever there’s a change.

void notifyClients(String state) {
  ws.textAll(state);
}

The AsyncWebSocket class provides a textAll() method for sending the same message to all clients that are connected to the server at the same time.

Handle WebSocket Messages

The handleWebSocketMessage() function is a callback function that will run whenever we receive new data from the clients via WebSocket protocol.

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    message = (char*)data;
    steps = message.substring(0, message.indexOf("&"));
    direction = message.substring(message.indexOf("&")+1, message.length());
    Serial.print("steps");
    Serial.println(steps);
    Serial.print("direction");
    Serial.println(direction);
    notifyClients(direction);
    notifyStop = true;
    if (direction == "CW"){
      Serial.print("CW");
      stepper.move(steps.toInt());
    }
    else{
      Serial.print("CCW");
      stepper.move(-steps.toInt());
    }
  }
}

We split the message to get the number of steps and direction.

message = (char*)data;
steps = message.substring(0, message.indexOf("&"));
direction = message.substring(message.indexOf("&")+1, message.length());

Then, we notify all clients of the motor direction so that all clients change the motor state on the web interface.

notifyClients(direction);

We set the notifyStop variable to true so that we’re able to notify that the motor has stopped later on.

notifyStop = true;

Then, set the number of steps you want the motor to move depending on the direction (negative number of steps for counterclockwise direction and a positive number of steps for clockwise direction). For that, we use the move() function. The move() function doesn’t actually move the motor, it simply sets the new number of steps we want to move the motor. The motor will then go to the desired position one step at a time using the run() function in the loop().

if (direction == "CW"){
  Serial.print("CW");
  stepper.move(steps.toInt());
}
else{
  Serial.print("CCW");
  stepper.move(-steps.toInt());
}

Note: the move() function sets the target position relative to the current position. The moveTo() function sets the absolute target position (each position is defined by a predetermined number of steps). For more information check the library documentation.

Configure the WebSocket server

Now we need to configure an event listener to handle the different asynchronous steps of the WebSocket protocol. This event handler can be implemented by defining the onEvent() as follows:

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  switch (type) {
    case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      //Notify client of motor current state when it first connects
      notifyClients(direction);
      break;
    case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
    case WS_EVT_DATA:
        handleWebSocketMessage(arg, data, len);
        break;
    case WS_EVT_PONG:
    case WS_EVT_ERROR:
     break;
  }
}

The type argument represents the event that occurs. It can take the following values:

  • WS_EVT_CONNECT when a client has logged in;
  • WS_EVT_DISCONNECT when a client has logged out;
  • WS_EVT_DATA when a data packet is received from the client;
  • WS_EVT_PONG in response to a ping request;
  • WS_EVT_ERROR when an error is received from the client.

There’s a section to notify any client of the current motor state when it first connects:

case WS_EVT_CONNECT:
  Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
  //Notify client of motor current state when it first connects
  notifyClients(direction);
  break;

Initialize WebSocket

Finally, the initWebSocket() function initializes the WebSocket protocol.

void initWebSocket() {
  ws.onEvent(onEvent);
  server.addHandler(&ws);
}

setup()

In the setup(), initialize the Serial Monitor.

Serial.begin(115200);

Call the initWiFi() function to initialize WiFi.

initWiFi();

Initialize websocket communication.

initWebSocket();

Call the initFS() function to initialize the filesystem.

initFS();

And set the stepper motor maximum speed and acceleration.

stepper.setMaxSpeed(1000);
stepper.setAcceleration(100);

Handle requests

Then, handle the web server. When you receive a request on the root (/) URL—this is when you access the ESP IP address— send the HTML text to build the web page:

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

When the HTML file loads on your browser, it will make a request for the CSS and JavaScript files. These are static files saved on the same directory (LittleFS). So, we can simply add the following line to serve files in a directory when requested by the root URL. It will serve the CSS and JavaScript files automatically.

server.serveStatic("/", LittleFS, "/");

Finally, start the server.

server.begin();

loop()

Let’s take a look at the loop() section.

In the loop() is where the motor moves one step at a time using the run() function. This function makes the motor move until it reaches its desired position (set by the move() function)).

stepper.run();

Additionally, there’s an if statement to check if we need to notify the clients that the motor has stopped. The distanceToGo() function returns how many steps are left until we reach the desired position. So, when it returns 0, it means the motor has stopped. We also check if the notifyStop variable is true (it means the motor was spinning previously).

if (stepper.distanceToGo() == 0 && notifyStop == true){  
  direction = "stop";
  notifyClients(direction);
  notifyStop = false;
}

When these conditions are met, we notify the clients the motor has stopped.

direction = "stop";
notifyClients(direction);

Finally, set the notifyStop variable to false. It will only be true again if the board receives a request to move the motor.

Upload Code and Files

After inserting your network credentials, save the code. Go to Sketch > Show Sketch Folder, and create a folder called data.

sketch show sketch folder arduino IDE

Inside that folder, you should save the HTML, CSS, and JavaScript files.

Then, upload the code to your ESP8266 board. Make sure you have the right board and COM port selected. Also, make sure you’ve added your network credentials.

Arduino 2.0 Upload Button

After uploading the code, you need to upload the files. Go to Tools ESP8266 LittleFS Data Upload and wait for the files to be uploaded.

ESP8266 Tools LittleFS Data Upload Arduino IDE

When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP8266 RST button, and it should print the ESP8266 IP address.

Demonstration

Open a web browser or multiple web browser windows on your local network and you’ll access the web page to control the motor. Submit the form to control the motor.

ESP8266 NodeMCU Stepper Motor Web Server Websocket Motor Spinning

The gear on the web page starts spinning in the right direction and the physical motor starts working.

28BYJ-48 connected to ULN2003 Motor Driver 01 module

When it stops, the gear on the web page and the motor state change accoridngly.

ESP8266 NodeMCU Stepper Motor Web Server Websocket Motor Stopped

Notice that if you have multiple clients connect, all clients update the motor state almost instantaneously.

Watch the video below for a live demonstration.


Wrapping Up

In this tutorial you’ve learned how to control a stepper motor using a web server built with the ESP8266. The web server provides a web page to control the stepper motor using a form whose results are sent to the ESP8266 via WebSocket protocol.

If you want to learn more about HTML, CSS, JavaScript, and client-server communication protocols to build your ESP32 and ESP8266 web servers from scratch as we’ve done in this tutorial, make sure you take a look at our eBook:

Check the following resources to learn more about the ESP8266 board:

We hope you find this tutorial useful.

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!

9 thoughts on “ESP8266 NodeMCU Web Server: Control Stepper Motor (WebSocket)”

  1. Thanks for posting this tutorial. You have added a bunch of functionality into one project – webserver, websockets, stepper motor control and webpage animation (the gear). You have provided all the key elements and therefore this makes for a great starting point for working with stepper motors via the ESP8266 webserver.
    Thanks again.

    Reply
  2. Hi Rui ans Sara, again a very good tutorial ! but I am puzzled with onEvent()…
    I understand that the onEvent() procedure plays a key role in the action, but I do not understand its origin. It does not appear in the .h included files.
    Is onEvent a special procedure name ? Is there a special way to write it ?
    How do we associate events with onEvent ?
    For instance, WiFi object has a onEvent method : the smart guy who developed the WiFi library has written some private code to associate events to the method, but in this sketch, onEvent is a procedure, then… I am lost
    Thank you for your help

    Reply
    • Hi.
      The onEvent() function is defined in the code itself.
      void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
      switch (type) {
      case WS_EVT_CONNECT:
      Serial.printf(“WebSocket client #%u connected from %s\n”, client->id(), client->remoteIP().toString().c_str());
      //Notify client of motor current state when it first connects
      notifyClients(direction);
      break;
      case WS_EVT_DISCONNECT:
      Serial.printf(“WebSocket client #%u disconnected\n”, client->id());
      break;
      case WS_EVT_DATA:
      handleWebSocketMessage(arg, data, len);
      break;
      case WS_EVT_PONG:
      case WS_EVT_ERROR:
      break;
      }
      }
      Then, we simply call that function later to use it.
      Regards,
      Sara

      Reply
  3. Hi, thanks very much for this tutorial.
    Can you advise how the entry box for number of steps can already contain a predefined value for whenever the page is loaded?
    Most the time I’m always using the same number so it would useful if it was already there.
    Kind regards,
    Will.

    Reply
  4. Hi,
    I want to thank you for this great tutorial. I’m totally newbie in this, but the steps in this tutorial are very clear so I can build this project successfully. I also learned how to upload files to the ESP8266, but how to delete unnecessary files already uploaded to the chip?
    Regards,
    Wawan

    Reply
  5. Hello great Santos’es (and maybe others)

    Very great and learnfull site, but here I’m stuck at the speedstepper,
    I want to http:///?direction=CCW&steps=20000 (for example)
    But I tryed about 10000 diff ways to send, but no response at all, only what I
    Type in on the website ‘Number of steps’ is the stepper responding

    Greetings from the Netherlands

    PS to bad I just missed the tutorials especialy the one to build a complete domotica system
    https://randomnerdtutorials.com/build-a-home-automation-system-for-100/

    Reply
    • Hi.
      If you want to pass the parameters on the URL, use the following tutorial instead: https://randomnerdtutorials.com/stepper-motor-esp32-web-server/
      We’re not selling that course anymore because some of the chapters are outdated-
      We’re currently working on a similar eBook, with new content, and with updated subjects. It will be available in September or October.
      So, stay tuned.
      Thank you for supporting our work.
      Regards,
      Sara

      Reply

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.