RoboCard: ESP32CAM on Wheels

RoboCard is powered by ESP32CAM. Its frame is made out of expired credit card, integrating sustainability with robotics.

ESP 32

Introduction

Welcome to the RoboCar Tutorial! Are you interested in exploring the exciting world of robotics? Look no further! In this comprehensive tutorial, we will guide you through the process of building a robot car using an ESP32CAM and a creative frame constructed from expired credit cards. This project combines the power of the ESP32CAM microcontroller with your very own custom-built robot car frame, resulting in a unique and interactive learning experience. Our tutorial is designed with beginners in mind, providing step-by-step instructions and explanations to help you grasp the fundamentals of robotics. Whether you are a hobbyist, student, or simply curious about robotics, this tutorial will equip you with the knowledge and skills to embark on your robotic journey. Throughout this tutorial, you will learn how to assemble the robot car, program its behavior, and control it using a custom app that we have developed. By the end of the tutorial, you will have a fully functional robot car that can be remotely controlled and capture images with the integrated ESP32CAM camera. Not only will you gain hands-on experience in building and programming a robot car, but you will also have the opportunity to unleash your creativity by repurposing everyday objects, such as expired credit cards, to construct the car's frame. This project combines innovation, sustainability, and learning in an exciting package. So, whether you're a robotics enthusiast or just getting started, join us on this journey as we delve into the fascinating world of robotics. Let's build, learn, and have fun together!

What You Will Need

To build this, you will need a few items. Below I have listed them out along with links to where you can find them on Cytron.

Part 1: Hardware

Let's start by building the car!

Step 1: Connecting the ESP32CAM and Motor Driver to Breadboard

We will have to attach the ESP32CAM and Motor Driver onto the mini breadboard as shown on the picture.





Note: You can click on the image to expand it!

ESP 32

Step 2: Connections between ESP32CAM and Motor Driver

Connect the ESP32CAM and Motor Driver with Jumper Wires as follows:

ESP32CAM > Motor Driver

  • IO15 > AIN1
  • IO3 > AIN2
  • IO13 > PWMA
  • IO14 > BIN1
  • IO2 > BIN2
  • IO12 > PWMB

Note: Use alternating colours for connections to prevent getting confused!

ESP 32

Step 3: Connecting ESP32CAM and Motor Driver to Power Rails

Connect the ESP32CAM and Motor Driver to the power rails as follows:

  • (ESP32CAM) 5V > 5V
  • (ESP32CAM) GND > GND
  • (Motor Driver) VM > 5V
  • (Motor Driver) VCC > 5V
  • (Motor Driver) STBY > 5V
  • (Motor Driver) GND (Both) > GND

Note: Use red for 5V and black/white for GND

ESP 32

Step 4: Assembling the wheels

You will have to fit in the wheels to the motor and solder wires to the motor





Note: Be careful of the HOT soldering iron! Seek help if you are unfamiliar with it!

ESP 32

Step 5: Preparing the credit cards!

Apply tape to the card as shown in the picture!

ESP 32

Step 6: Sticking the Motors!

Put the motors and foams onto one of the cards as shown. This will be your bottom plate!

Image 1 Image 2

Step 7: Sandwich the Motors and Add the Front Wheel!

Place the other card on top and stick them together. Then, stick the front wheel!

Image 1 Image 2

Step 8: Adding the USB to UART Converter!

Add the USB to UART Converter between the two cards as shown!

ESP 32

Step 9: Mounting the Breadboard and PowerBank!

Apply tape to the top of the frame and stick the breadboard and powerbank onto the frame!

ESP 32

Step 10: Connect the Motor to the Breadboard!

Connect the left motor's wire to AO1 and AO2 and the right motor's wire to BO1 and BO2!

ESP 32

Step 11: Connect the USB to UART Converter to the Breadboard!

Connect the USB to UART Converter's VCC to 5V and GND to GND as mentioned at Step3!

ESP 32

Step 12: Connect the Powerbank to the Converter!

Connect the PowerBank to the USB to UART Converter using a microUSB cable! Then, we are done building the car!

ESP 32

Part 2: Software

Next, We will build the software for the car!

Step 1: Downloading Arduino IDE

To start, you need to download the software from Arduino's website: https://www.arduino.cc/en/software

Arduino IDE

Step 2: Include Libraries and Configure Camera

We include the necessary libraries for this project and configure the camera

The following libraries are included in the code:

              
              #include "esp_camera.h"   // This library provides functions to initialize and control the ESP32 camera module.
              #include <WiFi.h> // This library enables Wi-Fi connectivity.
              #include <WebSocketsServer.h> // This library allows creating a WebSocket server.
              #include <AsyncTCP.h> // This library provides asynchronous TCP/IP communication.
              #include <ESPAsyncWebServer.h> // This library enables the creation of an asynchronous web server on ESP32.
              
            

Pin Definitions

The code defines various GPIO pin numbers for camera and other purposes:

              
              #define PWDN_GPIO_NUM     32
              #define RESET_GPIO_NUM    -1
              #define XCLK_GPIO_NUM      0
              #define SIOD_GPIO_NUM     26
              #define SIOC_GPIO_NUM     27
          
              #define Y9_GPIO_NUM       35
              #define Y8_GPIO_NUM       34
              #define Y7_GPIO_NUM       39
              #define Y6_GPIO_NUM       36
              #define Y5_GPIO_NUM       21
              #define Y4_GPIO_NUM       19
              #define Y3_GPIO_NUM       18
              #define Y2_GPIO_NUM        5
              #define VSYNC_GPIO_NUM    25
              #define HREF_GPIO_NUM     23
              #define PCLK_GPIO_NUM     22
              
            

Camera Configuration

A function named configCamera() is defined to configure the camera module:

              
              void configCamera(){
                camera_config_t config;
                // Camera pin configuration
                config.ledc_channel = LEDC_CHANNEL_0;
                config.ledc_timer = LEDC_TIMER_0;
                config.pin_d0 = Y2_GPIO_NUM;
                config.pin_d1 = Y3_GPIO_NUM;
                config.pin_d2 = Y4_GPIO_NUM;
                config.pin_d3 = Y5_GPIO_NUM;
                config.pin_d4 = Y6_GPIO_NUM;
                config.pin_d5 = Y7_GPIO_NUM;
                config.pin_d6 = Y8_GPIO_NUM;
                config.pin_d7 = Y9_GPIO_NUM;
                config.pin_xclk = XCLK_GPIO_NUM;
                config.pin_pclk = PCLK_GPIO_NUM;
                config.pin_vsync = VSYNC_GPIO_NUM;
                config.pin_href = HREF_GPIO_NUM;
                config.pin_sscb_sda = SIOD_GPIO_NUM;
                config.pin_sscb_scl = SIOC_GPIO_NUM;
                config.pin_pwdn = PWDN_GPIO_NUM;
                config.pin_reset = RESET_GPIO_NUM;
                config.xclk_freq_hz = 10000000;
                config.pixel_format = PIXFORMAT_JPEG;
          
                config.frame_size = FRAMESIZE_QVGA;
                config.jpeg_quality = 15;
                config.fb_count = 100;
          
                // Initialize the camera
                esp_err_t err = esp_camera_init(&config);
                if (err != ESP_OK) {
                  Serial.printf("Camera init failed with error 0x%x", err);
                  return;
                }
              }
            
          

Live Camera Function

A function named liveCam() is defined to capture and send a live camera frame:

              
              void liveCam(uint8_t num){
                // Capture a frame
                camera_fb_t *fb = esp_camera_fb_get();
                if (!fb) {
                  Serial.println("Frame buffer could not be acquired");
                  return;
                }
                
                // Replace this with your own function
                webSocket.sendBIN(num, fb->buf, fb->len);
          
                // Return the frame buffer back to be reused
                esp_camera_fb_return(fb);
              }
              
            

Step 3: Configuring WebSocket

The code defines a WebSocket server and handles WebSocket events. It uses the WebSocketsServer library to handle WebSocket connections and the AsyncWebServer library to create an asynchronous web server. Let's break down the code further.

                
                // Define WebSocket server on port 81
                WebSocketsServer webSocket = WebSocketsServer(81);
                
                // Create AsyncWebServer on port 80
                AsyncWebServer server(80);
                
                // Variable to store the WebSocket connection number
                uint8_t cam_num;
                
                // Boolean flag to indicate if a client is connected
                bool connected = false;
                
                // String variable to store received WebSocket message
                String message = "";
                
                // Function to handle WebSocket text messages
                void handleWebSocketMessage(uint8_t *data, size_t len) {
                  data[len] = 0;
                  message = (char*)data;
                  Serial.println(message);
                  moveMotor(); // Call a function to move the motor based on the received message
                }
                
                // Function to handle WebSocket events
                void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
                  switch(type) {
                    case WStype_DISCONNECTED:
                      Serial.printf("[%u] Disconnected!\n", num);
                      break;
                    case WStype_CONNECTED:
                      cam_num = num;
                      connected = true;
                      break;
                    case WStype_TEXT:
                      handleWebSocketMessage(payload, length);
                      break;
                    case WStype_BIN:
                    case WStype_ERROR:      
                    case WStype_FRAGMENT_TEXT_START:
                    case WStype_FRAGMENT_BIN_START:
                    case WStype_FRAGMENT:
                    case WStype_FRAGMENT_FIN:
                      break;
                  }
                }
              
            

In this code, a WebSocket server is created on port 81 using the WebSocketsServer library. An AsyncWebServer is also created on port 80 using the AsyncWebServer library. The variable cam_num is used to store the WebSocket connection number, and the connected flag indicates whether a client is connected or not.

The handleWebSocketMessage function is responsible for handling WebSocket text messages. It takes the received data as input, converts it to a null-terminated string, stores it in the message variable, and prints it to the serial monitor. It also calls a function named moveMotor() to move the motor based on the received message.

The webSocketEvent function is the WebSocket event handler. It is called when a WebSocket event occurs. It takes four parameters: num (the WebSocket connection number), type (the type of event), payload (the received data payload), and length (the length of the payload).

Inside the webSocketEvent function, a switch statement is used to handle different types of WebSocket events. If the event type is WStype_DISCONNECTED, it prints a message indicating that the client has been disconnected. If the event type is WStype_CONNECTED, it stores the WebSocket connection number in cam_num and sets the connected flag to true. If the event type is WStype_TEXT, it calls the handleWebSocketMessage function to process the received text message. Other event types are ignored in this code.

Step 4: Coding the web interface

This is the HTML code for the web interface, including the canvas element for displaying the camera feed and joystick control.

Web Interface
                
                String index_html = R"(
                  <!DOCTYPE html>
                  <html>
                  <head>
                  <title> WebSockets Client</title>
                  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                  <script src='http://code.jquery.com/jquery-1.9.1.min.js'></script>
                  </head>
                  <body scroll="no" style="overflow: hidden;position: fixed;
                  font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif ;
                  color:rgb(128, 128, 128);
                  font-size: xx-large;">
                      <p style="text-align: center;">
                          X: <span id="x_coordinate"> </span>
                          Y: <span id="y_coordinate"> </span> <br> <br>
                          <img id='live' src='' display>
                      </p>
                  <canvas id="canvas"></canvas>
                  </body>
                  </html>
                  <script>
                  jQuery(function($){
                      if (!('WebSocket' in window)) {
                          alert('Your browser does not support web sockets');
                      } else {
                          setup();
                      }
                  
                      function setup() {
                        var host = 'ws://server_ip:81';
                        var socket = new WebSocket(host);
                        socket.binaryType = 'arraybuffer';
                
                        if (socket) {
                            socket.onopen = function() {
                            }
                
                            socket.onmessage = function(msg) {
                                var bytes = new Uint8Array(msg.data);
                                var binary= '';
                                var len = bytes.byteLength;
                                for (var i = 0; i < len; i++) {
                                    binary += String.fromCharCode(bytes[i])
                                }
                
                                var img = document.getElementById('live');
                                img.src = 'data:image/jpg;base64,'+window.btoa(binary);
                            }
                
                            socket.onclose = function() {
                                showServerResponse('The connection has been closed.');
                            }
                        }
                
                        var x_relative, y_relative;
                
                        setInterval(function() {
                            var x = x_relative; // get x value here
                            var y = y_relative; // get y value here
                            send(x, y);
                        }, 100);
                
                        function send(x,y){
                            var data = {x,y};
                            data = JSON.stringify(data);
                            console.log(data);
                            socket.send(data);
                        }
                        
                        var canvas, ctx;
                
                        window.addEventListener('load', () => {
                
                            canvas = document.getElementById('canvas');
                            ctx = canvas.getContext('2d');          
                            resize(); 
                
                            document.addEventListener('mousedown', startDrawing);
                            document.addEventListener('mouseup', stopDrawing);
                            document.addEventListener('mousemove', Draw);
                
                            document.addEventListener('touchstart', startDrawing);
                            document.addEventListener('touchend', stopDrawing);
                            document.addEventListener('touchcancel', stopDrawing);
                            document.addEventListener('touchmove', Draw);
                            window.addEventListener('resize', resize);
                
                            document.getElementById("x_coordinate").innerText = 0;
                            document.getElementById("y_coordinate").innerText = 0;
                        });
                
                        var width, height, radius, x_orig, y_orig;
                        function resize() {
                            width = window.innerWidth;
                            radius = window.innerWidth/4;
                            height = radius * 6;
                            ctx.canvas.width = width;
                            ctx.canvas.height = height;
                            background();
                            joystick(width / 2, height / 3);
                        }
                
                        function background() {
                            x_orig = width / 2;
                            y_orig = height / 3;
                
                            ctx.beginPath();
                            ctx.arc(x_orig, y_orig, radius + 15, 0, Math.PI * 2, true);
                            ctx.fillStyle = '#ECE5E5';
                            ctx.fill();
                        }
                
                        function joystick(width, height) {
                            ctx.beginPath();
                            ctx.arc(width, height, radius, 0, Math.PI * 2, true);
                            ctx.fillStyle = '#3D85C6';
                            ctx.fill();
                            ctx.strokeStyle = '#9FC5E8';
                            ctx.lineWidth = 8;
                            ctx.stroke();
                        }
                
                        let coord = { x: 0, y: 0 };
                        let paint = false;
                
                        function getPosition(event) {
                            var mouse_x = event.clientX || event.touches[0].clientX;
                            var mouse_y = event.clientY || event.touches[0].clientY;
                            coord.x = mouse_x - canvas.offsetLeft;
                            coord.y = mouse_y - canvas.offsetTop;
                        }
                
                        function is_it_in_the_circle() {
                            var current_radius = Math.sqrt(Math.pow(coord.x - x_orig, 2) + Math.pow(coord.y - y_orig, 2));
                            if (radius >= current_radius) return true
                            else return false
                        }
                
                
                        function startDrawing(event) {
                            paint = true;
                            getPosition(event);
                            if (is_it_in_the_circle()) {
                                ctx.clearRect(0, 0, canvas.width, canvas.height);
                                background();
                                joystick(coord.x, coord.y);
                                Draw();
                            }
                        }
                
                
                        function stopDrawing() {
                            paint = false;
                            ctx.clearRect(0, 0, canvas.width, canvas.height);
                            background();
                            joystick(width / 2, height / 3);
                            document.getElementById("x_coordinate").innerText = 0;
                            document.getElementById("y_coordinate").innerText = 0;
                            x_relative = 0;
                            y_relative = 0;            
                
                        }
                
                        function Draw(event) {
                
                            if (paint) {
                                ctx.clearRect(0, 0, canvas.width, canvas.height);
                                background();
                                var angle_in_degrees, x, y;
                                var angle = Math.atan2((coord.y - y_orig), (coord.x - x_orig));
                
                                if (Math.sign(angle) == -1) {
                                    angle_in_degrees = Math.round(-angle * 180 / Math.PI);
                                }
                                else {
                                    angle_in_degrees =Math.round( 360 - angle * 180 / Math.PI);
                                }
                
                                if (is_it_in_the_circle()) {
                                    joystick(coord.x, coord.y);
                                    x = coord.x;
                                    y = coord.y;
                                }
                                else {
                                    x = radius * Math.cos(angle) + x_orig;
                                    y = radius * Math.sin(angle) + y_orig;
                                    joystick(x, y);
                                }
                
                            
                                getPosition(event);
                
                                x_relative = Math.round(x - x_orig);
                                y_relative = Math.round(y - y_orig);
                
                                document.getElementById("x_coordinate").innerText =  x_relative;
                                document.getElementById("y_coordinate").innerText =  y_relative ;
                            }
                        } 
                    }
                  });
                  </script>
                      
                  

Step 5: Configuring the Motors

This code provides a modular approach to control the speed and direction of two motors based on input values. It allows the motors to be adjusted within the defined minimum and maximum speed ranges and provides a function to stop both motors simultaneously.

                
                // variables for motor drivers
                #define minspd 80
                #define maxspd 150
                
                int leftMotor =0, rightMotor=0;
                int sped(int dSpeed){
                  int nspd;
                  if (abs(dSpeed)>100){
                    dSpeed > 0 ? dSpeed = 100 : dSpeed = -100;
                  }
                  if (dSpeed > 0){
                    nspd = map(dSpeed, 0, 100, minspd, maxspd);
                  }else if (dSpeed < 0){
                    nspd = map(dSpeed, -100, 0, -maxspd, -minspd);
                  }
                  else{
                    nspd = 0;
                  }
                  return nspd;
                }
                
                void motor1(int Hspd) {
                  int spd = sped(Hspd);
                  if (spd > 5) {
                    digitalWrite(motor1Pin1, HIGH);
                    digitalWrite(motor1Pin2, LOW);
                  }else if (spd < -5) {
                    digitalWrite(motor1Pin1, LOW);
                    digitalWrite(motor1Pin2, HIGH);
                    spd = -spd;
                  }else{
                    digitalWrite(motor1Pin1, LOW);
                    digitalWrite(motor1Pin2, LOW);
                  }
                  ledcWrite(pwmChannel_1, spd);
                }
                
                void motor2(int Hspd) {
                  int spd = sped(Hspd);
                  if (spd > 5) {
                    digitalWrite(motor2Pin1, HIGH);
                    digitalWrite(motor2Pin2, LOW);
                  }else if (spd < -5) {
                    digitalWrite(motor2Pin1, LOW);
                    digitalWrite(motor2Pin2, HIGH);
                    spd = -spd;
                  }else {
                    digitalWrite(motor2Pin1, LOW);
                    digitalWrite(motor2Pin2, LOW);
                  }
                  ledcWrite(pwmChannel_2, spd);
                }
                
                void stopAll() {
                  motor1(0);
                  motor2(0);
                }
                
                void moveMotor(){
                  if(message.length()>2){
                    x_reading = message.substring(message.indexOf(':')+1,message.indexOf(',')).toInt();
                    Serial.println(x_reading);
                    y_reading = message.substring(message.indexOf(',')+5,message.length()).toInt();
                    Serial.println(y_reading);
                  }
                  if (abs(y_reading)>15){
                    leftMotor = y_reading;
                    rightMotor = y_reading;
                  }else{
                    leftMotor = 0, rightMotor= 0;
                  }
                
                  if (abs(x_reading)>20){
                    rightMotor = rightMotor - x_reading;
                    leftMotor = leftMotor + x_reading;
                  }
                
                  motor1(leftMotor);
                  motor2(rightMotor);
                }
              
            

The code provided is for controlling the speed and direction of two motors. It includes several functions and variables to achieve this functionality:

  • #define minspd 80: This line defines a constant variable minspd with a value of 80.
  • #define maxspd 150: This line defines a constant variable maxspd with a value of 150.
  • int leftMotor = 0, rightMotor = 0;: These variables store the current speed values for the left and right motors, initially set to 0.
  • int sped(int dSpeed): This function takes an input speed value (dSpeed) and maps it to the appropriate range based on the predefined minimum and maximum speeds. It returns the mapped speed value.
  • void motor1(int Hspd): This function controls the first motor. It takes a speed value (Hspd) as input and sets the appropriate digital outputs and pulse width modulation (PWM) value for the motor control pins.
  • void motor2(int Hspd): This function controls the second motor. It takes a speed value (Hspd) as input and sets the appropriate digital outputs and PWM value for the motor control pins.
  • void stopAll(): This function stops both motors by calling motor1(0) and motor2(0) to set their speeds to 0.
  • void moveMotor(): This function is responsible for moving the motors based on received input. It extracts the x and y readings from a message, adjusts the motor speeds accordingly, and calls motor1() and motor2() with the updated speeds.

Step 6: Setup and Loop

This code sets up the initial configuration, including establishing a WiFi connection, configuring the web server and WebSocket server, and initializing the necessary pins and PWM for motor control. The loop() function continues to check for WebSocket events and perform live camera operations.

                
                void setup() {
                  Serial.begin(115200);    // Initializes the serial communication at a baud rate of 115200
                  WiFi.begin("Wifi", "WifiPassword");    // Connects to a WiFi network with the given SSID and password
                  Serial.println("");
                  while (WiFi.status() != WL_CONNECTED) {    // Waits until the WiFi connection is established
                      delay(500);
                      Serial.print(".");
                  }
                  Serial.println("");
                  String IP = WiFi.localIP().toString();    // Retrieves the local IP address of the device
                  Serial.print("IP address: " + IP);
              
                  index_html.replace("server_ip", IP);    // Replaces the placeholder "server_ip" in the HTML template with the actual IP address
              
                  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
                      request->send(200, "text/html", index_html);    // Handles the root URL request and sends the HTML template as a response
                  });
              
                  server.begin();    // Starts the web server
                  webSocket.begin();    // Starts the WebSocket server
                  webSocket.onEvent(webSocketEvent);    // Registers the WebSocket event handler function
                  configCamera();    // Configures the camera
              
                  // Sets the pins for motors as outputs
                  pinMode(motor1Pin1, OUTPUT);
                  pinMode(motor1Pin2, OUTPUT);
                  pinMode(enable1Pin, OUTPUT);
                  pinMode(motor2Pin1, OUTPUT);
                  pinMode(motor2Pin2, OUTPUT);
                  pinMode(enable2Pin, OUTPUT);
              
                  // Configures LED PWM functionalities
                  ledcSetup(pwmChannel_1, freq, motorResolution);
                  ledcSetup(pwmChannel_2, freq, motorResolution);
              
                  // Attaches the PWM channels to the corresponding GPIO pins
                  ledcAttachPin(enable1Pin, pwmChannel_1);
                  ledcAttachPin(enable2Pin, pwmChannel_2);
                }

                void loop() {
                  // http_resp();    // Uncommented code related to HTTP response, if available
                  webSocket.loop();    // Continuously checks for WebSocket events and handles them
                  liveCam(cam_num);    // Calls the function to perform live camera operations
                }
                
              

The provided code consists of two main functions: setup() and loop(). Here's an explanation of each function:

  • setup(): This function is executed once when the device starts up. It performs the following tasks:
    • Initializes the serial communication at a baud rate of 115200.
    • Connects to a WiFi network with the given SSID and password.
    • Waits until the WiFi connection is established.
    • Retrieves the local IP address of the device.
    • Replaces the placeholder "server_ip" in the HTML template with the actual IP address.
    • Registers a route handler for the root URL ("/") to handle HTTP GET requests and send the HTML template as a response.
    • Starts the web server.
    • Starts the WebSocket server.
    • Registers the WebSocket event handler function.
    • Configures the camera.
    • Sets the necessary pins for motor control as outputs.
    • Configures LED Pulse Width Modulation (PWM) functionalities for motor control.
    • Attaches the PWM channels to the corresponding GPIO pins.
  • loop(): This function is executed repeatedly in an infinite loop. It performs the following tasks:
    • Checks for WebSocket events and handles them.
    • Performs live camera operations using the provided liveCam() function.

Finally!

You can download the full code here: RoboCar Code and flash it onto your ESP32CAM!

Congratulations!

You have completed the project! Hope you enjoyed it and learned something new!