ESP32 Robot Car Websocket Diagram

ESP32 Robot Car Using Websockets

In this post, I am going to share how I created my own ESP32 Robot Car using Websockets.  Websockets is a full duplex communication protocol over a single TCP connection making it ideal to control our robot car with minimal delay due to latency.  I have used PlatformIO IDE in developing this project as it simplifies development of Internet of Things (IOT) project due to numerous code assist features.

We are going to control our robot car using our mobile phones.  I have forked the following DPad and OPad controller to make it suitable to controlling the movement of our ESP32 based Robot Car.

For a demo of this project please see the following youtube link

I really had fun working with ESP32 Microcontroller Unit (MCU) just like my last post about creating Home Automation Project. I will try to discuss important items about how my ESP32 Robot car works by discussing what websockets are first.

For those who likes watching video tutorials, I got you covered also.

What is websockets?

Before we move forward, let’s discuss a little bit what Websockets is. Websockets is a communication protocol between a web server and clients like browsers.  But other clients can be used like python or c++ clients. In this post, we are going to use Javascript in programming our client. The websocket server publishes an endpoint that the websocket clients use to connect.  Typically the endpoint is at this format 

“ws://host:port/path?query”.  

The client initiates a connection and when a connection is established then communication can start.  But unlike HTTP, websockets supports bidirectional full duplex sending and receiving of messages.  So both client and server can send messages between each other.  We can also have multiple clients but that is beyond the scope of this post.

The ESP32 will act as our Websockets server that will received websocket messages coming from our robot car that is sending websocket messages thru Javascript in our browser.

Why use websockets?

Why is websockets ideal for this type of Internet of Things(IOT) applications?  It’s because of the less latency offered by our websockets.  So if a message is sent from our mobile phone that that the car should stop then it should stop “real time” else it would crash.

As mentioned before, websockets supports full duplex bidirectional communication where messages are sent back and forth between server and client. But in this project, we are not going to use it as messages are sent back from our client only (browser) and our ESP32 Microcontroller (server) responds to it only.

There are other options like using bluetooth or the XMLHttpRequest(AJAX) or long polling but this is beyond the scope of this post.

Why build a ESP32 robot car?

Building robot car is cool! Period!  And it is way cooler if we are going to use our ESP32 MCU as our Websockets server because of its built in wifi capability feature.

We have different options to control our robot car like using Bluetooth or Web Server using the usual XMLHTTPRequest or AJAX.

But using Websockets is a preferred option due to the almost “real time” response and the possibility of “duplex” communication between our robot car and our controller.  Websockets is often used in chat application or streaming dashboards due to its nature of being “two way/ bidirectional” between a server and a web browser.

In this post, we are going to use our mobile phone browser to control the direction of our ESP32 robot car.  

In future post, we will be exploring websockets further in our future Internet of Things (IOT) project.

Design Diagram of our ESP32 Robot Car Using Websockets

In the diagram above, the ESP32 act as our websockets server.  The browser in our mobile phone acts as the websocket client.  Websockets messages coming from our browser is processed by our ESP32 Microcontroller.  It then sends control messages into our LM298 motor driver to control the rotation of our robot car wheels.  In addition to direction controls, we are going to control the speed of our robot car by using Pulse Width Modulation (PWM).

Whenever we click or touch the up, down , left or right in our smart phone then a websocket message is sent to our robot car and the ESP32 will control the direction.  The speed settings like “Slow”, “Normal” or “Fast” controls the speed on how our robot car moves.

In the image above, you would see that when I click any of the direction button then we are sending a websocket messages thru javascript.  Note that this is not the actual “websocket messages” but just console logs so when we click something and stop clicking then two messages are sent to the ESP32 Websocket server.

Why use PlatformIO?

I started exploring Internet of Things (IOT) project using Arduino IDE but I have a bit of a problem using it if you are going to use it in serious development.

First of all, Arduino IDE does not have “intellisense” support so it is much better if your code gets highlighted when something is wrong or have an autocomplete option.

Also, in Arduino IDE you have to manually configure all the libraries that you need.  On the other hand, in PlatformIO IDE you will just need to edit the platform.ini file and all libraries will be downloaded automatically for you.

I have coded lots of program in C++/Javascript/HTML/CSS in this ESP32 Robot car project using websockets and PlatformIO IDE really helped me code it faster because of its smart features.

Lastly, if your IOT project requires a lot of user interface then you need to create pages using HTML/CSS/Javascript.  Sadly, Arduino IDE fails in this category so I highly suggest that you explore PlatformIO IDE.  In the future, I will create my own post on how to get started with PlatformIO IDE.

Component List

To follow along with this project, you need to have the following parts and tools.

For the parts:

  • An ESP32 Microcontroller.  I used a 30Pin ESP32 generic board here
  • An LM298 Motor driver to control the two wheels of our robot car.
  • A robot car chassis.

For the Tools:

  • Battery pack to power our ESP32 and drive our motors
  • Breadboard
  • Connecting wires.

Schematic Diagram

The image above is the schematic diagram of our ESP32 Robot Car using websockets.

The ESP32, L298 and the TT motor gear should be connected according to the pin configuration in the table.  Make sure that the grounds are connected accordingly.

We used the same battery pack in powering our ESP32 MCU so the Vin should be connected to it as well.  But it is possible to have external power for both the ESP32 and the L298.

For the robot car chassis, I just purchased a common robot car chassis that you can purchased in the internet.  Just follow the direction on how to assemble it.

Code

The whole code is available in my github page in https://github.com/donskytech/platformio-projects/tree/main/esp32-projects/esp32-robot-car-websockets. You can either download the code as zip file and unzip it in your local directory or if you are familiar with git then you can clone it in your local directory.

Open up your visual studio code and make sure that PlatformIO extension is installed. In a new Visual Studio Code window, you can either Open Folder or Git Clone my repository.

Let us walk through the important stuff in the code. The first thing to look into is the platform.ini file. This is where we declare the required libraries used by our project. We only need the following libraries:

We also set the board to esp32dev and use the Arduino framework in programming. Also, we set the monitor_speed to 115200 so that we could see the messages in our serial monitor.

Let us subdivide the code to several sections as it will be hard to explain it in one go. We will need to code in the following languages

  • D-Pad/O-Pad Controller – HTML/CSS/Javascript
  • Websockets Server/Motor Control – C++ Arduino Code.

D-Pad/O-Pad Controller

Next, we discuss the important parts of our program. We will start with the user interface that we are using to control our robot car. For a complete demo on how I created my user interface then look at the following codepen. I have shared my whole html/css/javascript setup at this codepen that I have forked also. Thanks to the guy who created that very nice D-Pad and O-Pad controller!

First, we looked at the HTML code. You need to be familiar how to structure the user interface with HTML. We define our wrapper divs and created options for controlling our speed settings like “slow”, “normal” or “fast”.

Next, we setup our controllers like the D-Pad or the O-Pad. These are just links that gets styled using our Cascading Style Sheets (CSS). I won’t be discussing much about the CSS sections as these would need specialized discussions and this post would not be able to cover everything. I suggest you look into the internet for some information on how this works.

The most important thing to look at the HTML is the “data-direction” attribute as we will be manipulating this one in our javascript code.

<div class="parent">
  <div class="speed-settings">Speed Settings</div>
  <div class="wrapper">
    <input type="radio" name="speed-settings" id="option-1" value="slow">
    <input type="radio" name="speed-settings" id="option-2" value="normal">
    <input type="radio" name="speed-settings" id="option-3" value="fast">
    <label for="option-1" class="option option-1">
      <div class="dot"></div>
      <span>Slow</span>
    </label>
    <label for="option-2" class="option option-2">
      <div class="dot"></div>
      <span>Normal</span>
    </label>
    <label for="option-3" class="option option-3">
      <div class="dot"></div>
      <span>Fast</span>
    </label>
  </div>

  <div class="set blue">
    <nav class="d-pad">
      <a class="up control" data-direction="up"></a>
      <a class="right control" data-direction="right"></a>
      <a class="down control" data-direction="down"></a>
      <a class="left control" data-direction="left"></a>
    </nav>
    <nav class="o-pad">
      <a class="up control" data-direction="up"></a>
      <a class="right control" data-direction="right"></a>
      <a class="down control" data-direction="down"></a>
      <a class="left control" data-direction="left"></a>
    </nav>
  </div>

</div>

Next, we look at our javascript code. The goal of this post is to use our mobile phones in controlling our robot car so I created two functions to respond to those events. The touchStartHandler and touchEndHandler functions responds to a click in the link of our controller.

After which, we attached the two functions above event handler for each of the links. Same also with the speed settings radio button as we need to respond to “changes” in the speed settings

As mentioned above, we use the data-direction attribute of the link and send those direction as websocket messages so that we can control the direction of our robot car.

// Prevent scrolling on every click!

// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

function touchStartHandler(event) {
  console.log(event.target.dataset.direction);
  console.log("Touch Start!");
  // alert("Click!")
}

function touchEndHandler(event) {
  console.log(event.target.name);
  console.log("Touch End!");
  // alert("Click!")
}

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchstart", touchStartHandler);
});

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchend", touchEndHandler);
});

var speedSettings = document.querySelectorAll(
  'input[type=radio][name="speed-settings"]'
);
speedSettings.forEach((radio) =>
  radio.addEventListener("change", () => alert(radio.value))
);

Complete Code of our ESP32 Websockets Car Controller

The section above serves as the baseline of how our D-Pad and O-Pad controller will work. However, that is not complete as we need a way to send messages to our ESP32 Websockets Server from our browser.

Open your Visual Studio Code and locate the “data” folder. This is the direct link in my github account also.

The css folder contains our styles such as the min.css and our custom css. The js folder contains our custom.js which we will be using in creating our websocket client.

index.html

The index.html contains the code for the user interface of our controller. This is almost similar to the codepen that I have shared above. The only major difference is that it contains the necessary links for our custom.css and custom.js in the head and body section.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>ESP32 Robot Car Using Websocket</title>

    <link href="css/entireframework.min.css" rel="stylesheet" type="text/css">
    <link href="css/custom.css" rel="stylesheet" type="text/css">

</head>

<body>
    <nav class="nav" tabindex="-1" onclick="this.focus()">
        <div class="container">`
            <a class="pagename current" href="#">www.donskytech.com</a>
            <a href="#">One</a>
            <a href="#">Two</a>
            <a href="#">Three</a>
        </div>
    </nav>
    <button class="btn-close btn btn-sm">×</button>
    <div class="container">
        <div class="hero">
            <h2>ESP32 Robot Car Using Websockets</h2>
        </div>

        <div class="parent">
            <div class="speed-settings">Speed Settings</div>
            <div class="wrapper">
                <input type="radio" name="speed-settings" id="option-1" value="slow-speed" %SPEED_SLOW_STATUS%>
                <input type="radio" name="speed-settings" id="option-2" value="normal-speed" %SPEED_NORMAL_STATUS%>
                <input type="radio" name="speed-settings" id="option-3" value="fast-speed" %SPEED_FAST_STATUS%>
                <label for="option-1" class="option option-1">
                    <div class="dot"></div>
                    <span>Slow</span>
                </label>
                <label for="option-2" class="option option-2">
                    <div class="dot"></div>
                    <span>Normal</span>
                </label>
                <label for="option-3" class="option option-3">
                    <div class="dot"></div>
                    <span>Fast</span>
                </label>
            </div>

            <div class="set blue">
                <nav class="d-pad">
                    <a class="up control" data-direction="up"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="down"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
                <nav class="o-pad">
                    <a class="up control" data-direction="up"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="down"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
            </div>

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

</html>

The ESP32 Async Web Server resolves also the following “template” string. This is so that we would know which among the radio buttons we are going to select or “checked”.

<input type="radio" name="speed-settings" id="option-1" value="slow-speed" %SPEED_SLOW_STATUS%>
<input type="radio" name="speed-settings" id="option-2" value="normal-speed" %SPEED_NORMAL_STATUS%>
<input type="radio" name="speed-settings" id="option-3" value="fast-speed" %SPEED_FAST_STATUS%>

custom.js

The code below is our custom.js whose main function is to initiate a websocket client and send message to our ESP32 Websocket Server.

The onLoad function is called when our web page is loaded and its job is to initialize a websocket client from our browser to our ESP32 Websocket server.

The functions initializeSocket, onOpen, onClose, onMessage and sendMessage are used to log messages coming and going to our websocket client to the browser console. Also, it is used to send websocket message to our websocket server also.

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


function onLoad() {
  initializeSocket();
}

function initializeSocket() {
  console.log('Opening WebSocket connection to ESP32...');
  websocket = new WebSocket(targetUrl);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}
function onOpen(event) {
  console.log('Starting connection to server..');
}
function onClose(event) {
  console.log('Closing connection to server..');
  setTimeout(initializeSocket, 2000);
}
function onMessage(event) {
  console.log("WebSocket message received:", event)
}

function sendMessage(message) {
  websocket.send(message);
}

/*
Speed Settings Handler
*/
var speedSettings = document.querySelectorAll(
  'input[type=radio][name="speed-settings"]'
);
speedSettings.forEach((radio) =>
  radio.addEventListener("change", () => 
  {
    var speedSettings = radio.value;
    console.log('Speed Settings :: ' + speedSettings)
    sendMessage(speedSettings); 
  }
));


/*
O-Pad/ D-Pad Controller and Javascript Code
*/
// Prevent scrolling on every click!
// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

function touchStartHandler(event) {
  var direction = event.target.dataset.direction;
  console.log('Touch Start :: ' + direction)
  sendMessage(direction);
}

function touchEndHandler(event) {
  const stop_command = 'stop';
  var direction = event.target.dataset.direction;
  console.log('Touch End :: ' + direction)
  sendMessage(stop_command);
}


document.querySelectorAll('.control').forEach(item => {
  item.addEventListener('touchstart', touchStartHandler)
})

document.querySelectorAll('.control').forEach(item => {
  item.addEventListener('touchend', touchEndHandler)
})

A websocket message is sent also whenever we clicked the speed settings radio button.

/*
Speed Settings Handler
*/
var speedSettings = document.querySelectorAll(
  'input[type=radio][name="speed-settings"]'
);
speedSettings.forEach((radio) =>
  radio.addEventListener("change", () => 
  {
    var speedSettings = radio.value;
    console.log('Speed Settings :: ' + speedSettings)
    sendMessage(speedSettings); 
  }
));

Lastly, the following functions sends out the messages to our websocket server and also whenever we click the up, down, left and right buttons of our O-Pad and D-Pad.

/*
O-Pad/ D-Pad Controller and Javascript Code
*/
// Prevent scrolling on every click!
// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

function touchStartHandler(event) {
  var direction = event.target.dataset.direction;
  console.log('Touch Start :: ' + direction)
  sendMessage(direction);
}

function touchEndHandler(event) {
  const stop_command = 'stop';
  var direction = event.target.dataset.direction;
  console.log('Touch End :: ' + direction)
  sendMessage(stop_command);
}

That is all for the code in our O-Pad or D-Pad controller. This will serve as our websocket client that will send messages to our ESP32 Websocket Server to control our Robot Car.

main.cpp

The code below is our ESP32 program written in Arduino framework that will control our robot car. It contains the Websocket server that will respond to websocket messages coming from our browser.

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"

/*
  The resolution of the PWM is 8 bit so the value is between 0-255
  We will set the speed between 200 to 255.
*/
enum speedSettings
{
  SLOW = 200,
  NORMAL = 225,
  FAST = 255
};

class Car
{
private:
  // Motor 1 connections
  int in1 = 16;
  int in2 = 17;
  // Motor 2 connections
  int in3 = 32;
  int in4 = 33;

  // PWM Setup to control motor speed
  const int SPEED_CONTROL_PIN_1 = 21;
  const int SPEED_CONTROL_PIN_2 = 22;
  const int freq = 30000;
  const int channel_0 = 0;
  const int channel_1 = 1;
  const int resolution = 8;

  speedSettings currentSpeedSettings;

public:
  Car()
  {
    // Set all pins to output
    pinMode(in1, OUTPUT);
    pinMode(in2, OUTPUT);
    pinMode(in3, OUTPUT);
    pinMode(in4, OUTPUT);
    pinMode(SPEED_CONTROL_PIN_1, OUTPUT);
    pinMode(SPEED_CONTROL_PIN_2, OUTPUT);

    // Set initial motor state to OFF
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);

    //Attach Pin to Channel
    ledcAttachPin(SPEED_CONTROL_PIN_1, channel_0);
    ledcAttachPin(SPEED_CONTROL_PIN_2, channel_1);

    //Set the PWM Settings
    ledcSetup(channel_0, freq, resolution);
    ledcSetup(channel_1, freq, resolution);

    // initialize default speed to SLOW
    setCurrentSpeed(speedSettings::NORMAL);
  }
  void turnLeft()
  {
    Serial.println("car is turning left...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);

    setMotorSpeed();
  }
  void turnRight()
  {
    Serial.println("car is turning right...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, HIGH);
    setMotorSpeed();
  }
  void moveForward()
  {
    Serial.println("car is moving forward...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    digitalWrite(in3, LOW);
    digitalWrite(in4, HIGH);
    setMotorSpeed();
  }
  void moveBackward()
  {
    setMotorSpeed();
    Serial.println("car is moving backward...");
    digitalWrite(in1, HIGH);
    digitalWrite(in2, LOW);
    digitalWrite(in3, HIGH);
    digitalWrite(in4, LOW);

  }
  void stop()
  {
    Serial.println("car is stopping...");
    // Turn off motors
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);
  }

  void setMotorSpeed()
  {
    // change the duty cycle of the speed control pin connected to the motor
    Serial.print("Speed Settings: ");
    Serial.println(currentSpeedSettings);
    ledcWrite(channel_0, currentSpeedSettings);
    ledcWrite(channel_1, currentSpeedSettings);
  }

  void setCurrentSpeed(speedSettings newSpeedSettings)
  {
    Serial.println("car is changing speed...");
    currentSpeedSettings = newSpeedSettings;
  }

  speedSettings getCurrentSpeed()
  {
    return currentSpeedSettings;
  }
};

// Change this to your network SSID
const char *ssid = "<CHANGE THIS TO YOUR SSID>";
const char *password = "<CHANGE THIS TO YOUR PASSWORD>";

// AsyncWebserver runs on port 80 and the asyncwebsocket is initialize at this point also
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

// Our car
Car car;

void sendCarCommand(const char *command)
{
  // command could be either "left", "right", "forward" or "reverse" or "stop"
  // or speed settingg "slow-speed", "normal-speed", or "fast-speed"
  if (strcmp(command, "left") == 0)
  {
    car.turnLeft();
  }
  else if (strcmp(command, "right") == 0)
  {
    car.turnRight();
  }
  else if (strcmp(command, "up") == 0)
  {
    car.moveForward();
  }
  else if (strcmp(command, "down") == 0)
  {
    car.moveBackward();
  }
  else if (strcmp(command, "stop") == 0)
  {
    car.stop();
  }
  else if (strcmp(command, "slow-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::SLOW);
  }
  else if (strcmp(command, "normal-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::NORMAL);
  }
  else if (strcmp(command, "fast-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::FAST);
  }
}

// Processor for index page template
String indexPageProcessor(const String &var)
{
  String status = "";
  if (var == "SPEED_SLOW_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::SLOW)
    {
      status = "checked";
    }
  }
  else if (var == "SPEED_NORMAL_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::NORMAL)
    {
      status = "checked";
    }
  }
  else if (var == "SPEED_FAST_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::FAST)
    {
      status = "checked";
    }
  }
  return status;
}

void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
               void *arg, uint8_t *data, size_t len)
{
  switch (type)
  {
  case WS_EVT_CONNECT:
  {
    Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    // client->printf("Hello Client %u :)", client->id());
    // client->ping();
  }

  case WS_EVT_DISCONNECT:
  {
    Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
  }

  case WS_EVT_DATA:
  {
    //data packet
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    if (info->final && info->index == 0 && info->len == len)
    {
      //the whole message is in a single frame and we got all of it's data
      if (info->opcode == WS_TEXT)
      {
        data[len] = 0;
        char *command = (char *)data;
        sendCarCommand(command);
      }
    }
  }

  case WS_EVT_PONG:
  {
    Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  }

  case WS_EVT_ERROR:
  {
    // Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  }
  }
}

void notFound(AsyncWebServerRequest *request)
{
  request->send(404, "text/plain", "Not found");
}

void setup()
{
  Serial.begin(115200);
  Serial.println("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    Serial.printf("WiFi Failed!\n");
    return;
  }

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Initialize SPIFFS
  if (!SPIFFS.begin(true))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  ws.onEvent(onWsEvent);
  server.addHandler(&ws);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            {
              Serial.println("Requesting index page...");
              request->send(SPIFFS, "/index.html", "text/html", false, indexPageProcessor);
            });

  // Route to load entireframework.min.css file
  server.on("/css/entireframework.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/css/entireframework.min.css", "text/css"); });

  // Route to load custom.css file
  server.on("/css/custom.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/css/custom.css", "text/css"); });

  // Route to load custom.js file
  server.on("/js/custom.js", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/js/custom.js", "text/javascript"); });

  // On Not Found
  server.onNotFound(notFound);

  // Start server
  server.begin();
}

void loop()
{

}



We need to edit the following lines of code before we deploy this to our ESP32 MCU. Change this according to your Wifi setup so that your ESP32 could connect to your network.

// Change this to your network SSID
const char *ssid = "<CHANGE THIS TO YOUR SSID>";
const char *password = "<CHANGE THIS TO YOUR PASSWORD>";

Let us go to each line of code to discuss what it is doing.

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"

The code above contains the necessary include statements. The ESPAsyncWebServer contains a websocket server that runs on the same port 80 of our Web Server without additional code.

/*
  The resolution of the PWM is 8 bit so the value is between 0-255
  We will set the speed between 200 to 255.
*/
enum speedSettings
{
  SLOW = 200,
  NORMAL = 225,
  FAST = 255
};

These are enumeration that we assign for the speed settings. We are using PWM in ESP32 and we have set the resolution to 8 bit. The values are between 0 to 255. We have set the minimum speed to 200 as the robot car wheels will not move if the duty cycle is below this values.

class Car
{
private:
  // Motor 1 connections
  int in1 = 16;
  int in2 = 17;
  // Motor 2 connections
  int in3 = 32;
  int in4 = 33;

  // PWM Setup to control motor speed
  const int SPEED_CONTROL_PIN_1 = 21;
  const int SPEED_CONTROL_PIN_2 = 22;
  const int freq = 30000;
  const int channel_0 = 0;
  const int channel_1 = 1;
  const int resolution = 8;

  speedSettings currentSpeedSettings;

Car class

We have declared a C++ class to represent our car and is being used in Object Oriented programming per se. Think of a class as a blueprint of a housing unit. We can use the class to create several house using the same blueprint. Also, we can hide all functionality in this object so that our code would be easier to read. The “private” marker means that the following attributes are accessible only from this class.

We have defined several pin connections here including the frequency, channel and resolution for the Pulse Width Modulation of our robot car speed. Controlling the ENA and ENB pin of the L298 motor driver thru PWM will slow down or speed up our car.

public:
  Car()
  {
    // Set all pins to output
    pinMode(in1, OUTPUT);
    pinMode(in2, OUTPUT);
    pinMode(in3, OUTPUT);
    pinMode(in4, OUTPUT);
    pinMode(SPEED_CONTROL_PIN_1, OUTPUT);
    pinMode(SPEED_CONTROL_PIN_2, OUTPUT);

    // Set initial motor state to OFF
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);

    //Attach Pin to Channel
    ledcAttachPin(SPEED_CONTROL_PIN_1, channel_0);
    ledcAttachPin(SPEED_CONTROL_PIN_2, channel_1);

    //Set the PWM Settings
    ledcSetup(channel_0, freq, resolution);
    ledcSetup(channel_1, freq, resolution);

    // initialize default speed to NORMAL
    setCurrentSpeed(speedSettings::NORMAL);
  }

The code above with the public marker is called the constructor of our Car object. In this part of the code we initialize the necessary pins for the pinMode and setup the PWM setings of our ENA and ENB pins of our L298 Motor Driver module.

Also, we have initialized the speed settings to normal.

void turnLeft()
  {
    Serial.println("car is turning left...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);

    setMotorSpeed();
  }
  void turnRight()
  {
    Serial.println("car is turning right...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, HIGH);
    setMotorSpeed();
  }
  void moveForward()
  {
    Serial.println("car is moving forward...");
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
    digitalWrite(in3, LOW);
    digitalWrite(in4, HIGH);
    setMotorSpeed();
  }
  void moveBackward()
  {
    setMotorSpeed();
    Serial.println("car is moving backward...");
    digitalWrite(in1, HIGH);
    digitalWrite(in2, LOW);
    digitalWrite(in3, HIGH);
    digitalWrite(in4, LOW);

  }
  void stop()
  {
    Serial.println("car is stopping...");
    // Turn off motors
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);
  }

  void setMotorSpeed()
  {
    // change the duty cycle of the speed control pin connected to the motor
    Serial.print("Speed Settings: ");
    Serial.println(currentSpeedSettings);
    ledcWrite(channel_0, currentSpeedSettings);
    ledcWrite(channel_1, currentSpeedSettings);
  }

  void setCurrentSpeed(speedSettings newSpeedSettings)
  {
    Serial.println("car is changing speed...");
    currentSpeedSettings = newSpeedSettings;
  }

  speedSettings getCurrentSpeed()
  {
    return currentSpeedSettings;
  }

This part of the code is where we set how our car class will move. Think of it like this, we have created a new datatype or structure in C++ that is customized for our car object. A car object can move forward or turn left or right and even can movebackward. We have set the movement of our motors in each of the following methods turnLeft, turnRight, moveForward, moveBackward, stop, setMotorSpeed. We are also able to set the speed of our car using the setCurrentSpeed and getCurrentSpeed method.

When we click the up button in our mobile phone then we are going to call car.moveForward() and if we stop clicking then we will call car.stop(). Think of how much better we have arranged our code this way rather than creating multiple unrelated functions.

// Our car
Car car;

This is the line of code where we define our class to represent our car object.

ESP32 Websockets Server

We are going to discuss how we have setup our webscoket server in the following code.

// AsyncWebserver runs on port 80 and the asyncwebsocket is initialize at this point also
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

We have defined an AsyncWebServer and AsyncWebSocket running in the same port. The websocket server runs in the same port as our web server without additional code.

void sendCarCommand(const char *command)
{
  // command could be either "left", "right", "forward" or "reverse" or "stop"
  // or speed settingg "slow-speed", "normal-speed", or "fast-speed"
  if (strcmp(command, "left") == 0)
  {
    car.turnLeft();
  }
  else if (strcmp(command, "right") == 0)
  {
    car.turnRight();
  }
  else if (strcmp(command, "up") == 0)
  {
    car.moveForward();
  }
  else if (strcmp(command, "down") == 0)
  {
    car.moveBackward();
  }
  else if (strcmp(command, "stop") == 0)
  {
    car.stop();
  }
  else if (strcmp(command, "slow-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::SLOW);
  }
  else if (strcmp(command, "normal-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::NORMAL);
  }
  else if (strcmp(command, "fast-speed") == 0)
  {
    car.setCurrentSpeed(speedSettings::FAST);
  }
}

The function above sends out a command to our car object. The command comes from the wbsocket messages coming from our websocket client running in our browser. As I have mentioned above, whatever websocket message we received then we just forward it to our car object.

// Processor for index page template
String indexPageProcessor(const String &var)
{
  String status = "";
  if (var == "SPEED_SLOW_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::SLOW)
    {
      status = "checked";
    }
  }
  else if (var == "SPEED_NORMAL_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::NORMAL)
    {
      status = "checked";
    }
  }
  else if (var == "SPEED_FAST_STATUS")
  {
    if (car.getCurrentSpeed() == speedSettings::FAST)
    {
      status = "checked";
    }
  }
  return status;
}

The templating engine of the ESP32 Websocket server uses the function above. Remember in the index.html section I have discussed about the following section. This function will return either a “checked” attribute to our radio button to set the value of our speed settings.

<input type="radio" name="speed-settings" id="option-1" value="slow-speed" %SPEED_SLOW_STATUS%>
<input type="radio" name="speed-settings" id="option-2" value="normal-speed" %SPEED_NORMAL_STATUS%>
<input type="radio" name="speed-settings" id="option-3" value="fast-speed" %SPEED_FAST_STATUS%>
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
               void *arg, uint8_t *data, size_t len)
{
  switch (type)
  {
  case WS_EVT_CONNECT:
  {
    Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    // client->printf("Hello Client %u :)", client->id());
    // client->ping();
  }

  case WS_EVT_DISCONNECT:
  {
    Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
  }

  case WS_EVT_DATA:
  {
    //data packet
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    if (info->final && info->index == 0 && info->len == len)
    {
      //the whole message is in a single frame and we got all of it's data
      if (info->opcode == WS_TEXT)
      {
        data[len] = 0;
        char *command = (char *)data;
        sendCarCommand(command);
      }
    }
  }

  case WS_EVT_PONG:
  {
    Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  }

  case WS_EVT_ERROR:
  {
    // Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  }
  }

This is our callback function that will received messages coming from our O-Pad and D-Pad controller in our mobile phone. As you can see in the WS_EVT_DATA case, we send car command here by calling the sendCarCommand(command) function. Command could either be “left”, right”, “stop” etc.

void setup()
{
  Serial.begin(115200);
  Serial.println("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    Serial.printf("WiFi Failed!\n");
    return;
  }

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Initialize SPIFFS
  if (!SPIFFS.begin(true))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  ws.onEvent(onWsEvent);
  server.addHandler(&ws);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            {
              Serial.println("Requesting index page...");
              request->send(SPIFFS, "/index.html", "text/html", false, indexPageProcessor);
            });

  // Route to load entireframework.min.css file
  server.on("/css/entireframework.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/css/entireframework.min.css", "text/css"); });

  // Route to load custom.css file
  server.on("/css/custom.css", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/css/custom.css", "text/css"); });

  // Route to load custom.js file
  server.on("/js/custom.js", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/js/custom.js", "text/javascript"); });

  // On Not Found
  server.onNotFound(notFound);

  // Start server
  server.begin();
}

The next part is the setup() function where we initialize our web server and does the following things:

Lines:

  • 4-8 – Initialize our serial monitor with the correct baud rate as we have configured in our platform.ini.
  • 9-16 – Connect to our wifi.
  • 19-24 – Initialize SPIFFS to load our files.
  • 25-26 – Initializes our websocket server and add the onWsEvent as the event handler.
  • 28-44 – Configures our routing to our static assets like our javascript and custom css files
  • 47-50 – Start our server
void loop()
{

}

The loop method does not have any method in it as our web server is running in asynchronous mode.

Deploying our ESP32 Robot Car Websockets Server

Now that I have explained the program on how we are going to control our robot car using ESP32 Websockets then let’s explain how to deploy it to our ESP32 memory. Make sure that you have followed the wiring diagram that I have created above. You can disconnect first the battery connection from the Vin of the ESP32 while we are deploying it.

First, open up your Visual Studio Code and click the PlatformIO extension link at the side then click the Upload File System Image link. This will upload our html/css/javascipt files located in our “data” folder. Click the boot button on the ESP32 if you see that it is not uploading.

Secondly, when the previous step is successful then click the “Upload and Monitor” link. This will upload our main.cpp program to the ESP32 file system also.

Finally, if everything goes well then you would see in your monitor the IP address of your ESP32. If it says, “Wifi Failed” then just click the reset button. Open up the web browser in your mobile phones and type in the assigned IP address. You would see our D-Pad and O-Pad controller and see if you are able to control your robot car.

Wrap Up

In this post I have described how I have created and designed my ESP32 Robot Car using Websockets. I have described what websocket is and how it differs from the normal http protocol. We have taken advantage of its almost “low latency” response time in controlling our robot car. If for example you have an Internet of Things (IOT) project that needs real time delivery of information like sensor data then I highly suggest using websocket.

I really enjoyed working on this project so I have shared everything about it. Hope you find something of value in here. If you have any comments or suggestions then please consider connecting with me in any of my social media channels.

Happy Exploring!

If you like my post then please consider sharing this. Thanks!

donsky

I like IOT nowadays! Let us explore it together!

Leave a Reply

Your email address will not be published. Required fields are marked *