ESP32 CAM Car – Điều khiển robot từ xa qua Web Server

Trong dự án ESP32 CAM Car lần này, cùng tìm hiểu cách chế tạo và lập trình robot Car điều khiển từ xa qua WiFi với ESP32 CAM. Chúng ta có thể dùng Web Server để hiển thị video về khung cảnh mà robot “nhìn thấy”. Từ đó, bạn có thể điều khiển robot từ xa và quan sát những gì đang có xung quanh nó, kể cả khi nó nằm ngoài tầm nhìn.

ESP32 CAM Car - Điều khiển robot từ xa qua Web Server

Lưu ý: Dự án ESP32 CAM Car này yêu cầu mạch phải có 4 GPIO điều khiển động cơ DC. Do đó, bạn có thể dùng các loại mạch ESP32 CAM có sẵn 4 GPIO như ESP32 CAM AI-Thinker hoặc TTGO TJournal.

Tổng quan dự án ESP32 CAM Car

Trước khi đi vào hướng dẫn chi tiết, mời bạn cùng xem qua các tính năng và thành phần quan trọng trong việc chế tạo robot với ESP32 trong phần này:

WiFi

Robot sẽ được điều khiển từ xa qua WiFi, thông qua mạch ESP32-CAM của bạn.

Trong dự án này, IoTZone sẽ tạo một giao diện Web Server để điều khiển robot, bạn có thể truy cập vào Web Server từ bất kỳ thiết bị nào trong mạng cục bộ.

Trên Web Server cũng hiển thị video về những gì robot “nhìn thấy”. Để video có tốc độ truyền phát tốt nhất, bạn nên cân nhắc sử dụng ESP32-CAM có ăng ten bên ngoài nhé!

Điều khiển ESP32 CAM Car bằng CAM có ăng ten

Cách điều khiển robot qua Web Server

Trên Server chúng ta tạo sẽ có 5 nút nhấn khác nhau, đại diện cho việc di chuyển 4 hướng và dừng lại. Khi bạn nhấn vào nút nào, robot sẽ hoạt động tương ứng:

Cách điều khiển ESP32 CAM Car qua Web Server

Mình có tạo thêm nút dừng, vì chúng sẽ giúp ích trong trường hợp ESP32 không nhận được lệnh dừng, dù cho bạn đã thả nút điều khiển di chuyển.

Bộ khung robot

Bạn cần có 1 bộ khung robot phù hợp, gồm thân robot và bánh xe. Đa số các cửa hàng trực tuyến trên mạng hiện nay đều có bán bộ khung này, chúng có giá khá rẻ và cũng rất dễ lắp ráp.

Bạn có thể sử dụng bất kỳ bộ khung nào (không nhất thiết là bộ giống y chang của mình), miễn sao chúng có 2 động cơ DC là được:

Bộ khung robot cần có 2 động cơ DC để điều khiển 2 bánh xe
Bộ khung robot cần có 2 động cơ DC để điều khiển 2 bánh xe

L298N Motor Driver

Đây là module giúp chúng ta dễ dàng điều khiển động cơ DC trong các dự án chế tạo robot với ESP32. Bạn có thể điều khiển tốc độ và hướng của 2 động cơ:

Chế tạo robot với ESP32 bằng L298N Motor Driver

Trên Website IoTZone mình đã có bài viết khá chi tiết về cách hoạt động và cách sử dụng trình điều khiển động cơ L298N rồi, nên mình sẽ không nhắc lại ở đây. Bạn có thể xem qua bài viết đó ở link dưới để hiểu hơn về cách dùng L298N Motor Driver nha:

Nguồn điện

Để đơn giản hóa dự án ESP32 CAM Car, chúng ta sẽ cấp nguồn cho robot (động cơ) và ESP32 bằng cùng 1 nguồn điện. Mình dùng bộ sạc dự phòng và chúng hoạt động khá tốt.

Lưu ý: Động cơ tiêu thụ lượng điện cao, do đó nếu thấy robot di chuyển chập chờn hoặc không ổn định, bạn có thể sử dụng nguồn điện bên ngoài cho động cơ. Khi đó đồng nghĩa với việc bạn phải dùng 2 nguồn điện khác nhau, 1 cái cấp nguồn cho ESP32 và 1 cái cấp nguồn cho động cơ DC.

Không lan man nữa, bây giờ hãy cùng vào phần hướng dẫn chính về dự án ESP32 CAM Car nào!

Chuẩn bị

  • ESP32 CAM AI Thinker có ăng ten bên ngoài
  • L298N Motor Driver
  • Bộ khung robot
  • Sạc dự phòng hoặc nguồn điện 5V khác
  • Prototyping circuit board (tùy chọn)

Chương trình lập trình

Bạn copy đoạn code sau vào Arduino IDE của bạn, sau đó thay đổi thông tin mạng WiFi thành của bạn rồi nạp vào ESP32. Code sẽ hoạt động ngay lập tức!

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"             // disable brownout problems
#include "soc/rtc_cntl_reg.h"    // disable brownout problems
#include "esp_http_server.h"

// Thay thế thành mạng WiFi của bạn
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

#define PART_BOUNDARY "123456789000000000000987654321"

#define CAMERA_MODEL_AI_THINKER
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WITHOUT_PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM_B
//#define CAMERA_MODEL_WROVER_KIT

#if defined(CAMERA_MODEL_WROVER_KIT)
  #define PWDN_GPIO_NUM    -1
  #define RESET_GPIO_NUM   -1
  #define XCLK_GPIO_NUM    21
  #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      19
  #define Y4_GPIO_NUM      18
  #define Y3_GPIO_NUM       5
  #define Y2_GPIO_NUM       4
  #define VSYNC_GPIO_NUM   25
  #define HREF_GPIO_NUM    23
  #define PCLK_GPIO_NUM    22

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

#elif defined(CAMERA_MODEL_M5STACK_WITHOUT_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       17
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_AI_THINKER)
  #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

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

#else
  #error "Camera model not selected"
#endif

#define MOTOR_1_PIN_1    14
#define MOTOR_1_PIN_2    15
#define MOTOR_2_PIN_1    13
#define MOTOR_2_PIN_2    12

static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;

static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
  <head>
    <title>ESP32-CAM Robot</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;}
      table { margin-left: auto; margin-right: auto; }
      td { padding: 8 px; }
      .button {
        background-color: #2f4468;
        border: none;
        color: white;
        padding: 10px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 18px;
        margin: 6px 3px;
        cursor: pointer;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        -webkit-tap-highlight-color: rgba(0,0,0,0);
      }
      img {  width: auto ;
        max-width: 100% ;
        height: auto ; 
      }
    </style>
  </head>
  <body>
    <h1>ESP32-CAM Robot</h1>
    <img src="" id="photo" >
    <table>
      <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('forward');" ontouchstart="toggleCheckbox('forward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Forward</button></td></tr>
      <tr><td align="center"><button class="button" onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Left</button></td><td align="center"><button class="button" onmousedown="toggleCheckbox('stop');" ontouchstart="toggleCheckbox('stop');">Stop</button></td><td align="center"><button class="button" onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Right</button></td></tr>
      <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('backward');" ontouchstart="toggleCheckbox('backward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Backward</button></td></tr>                   
    </table>
   <script>
   function toggleCheckbox(x) {
     var xhr = new XMLHttpRequest();
     xhr.open("GET", "/action?go=" + x, true);
     xhr.send();
   }
   window.onload = document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
  </script>
  </body>
</html>
)rawliteral";

static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
    //Serial.printf("MJPG: %uB\n",(uint32_t)(_jpg_buf_len));
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};
  
  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "go", variable, sizeof(variable)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  sensor_t * s = esp_camera_sensor_get();
  int res = 0;
  
  if(!strcmp(variable, "forward")) {
    Serial.println("Forward");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else if(!strcmp(variable, "left")) {
    Serial.println("Left");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else if(!strcmp(variable, "right")) {
    Serial.println("Right");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  }
  else if(!strcmp(variable, "backward")) {
    Serial.println("Backward");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  }
  else if(!strcmp(variable, "stop")) {
    Serial.println("Stop");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 0);
  }
  else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t cmd_uri = {
    .uri       = "/action",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };
  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }
  config.server_port += 1;
  config.ctrl_port += 1;
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
  
  pinMode(MOTOR_1_PIN_1, OUTPUT);
  pinMode(MOTOR_1_PIN_2, OUTPUT);
  pinMode(MOTOR_2_PIN_1, OUTPUT);
  pinMode(MOTOR_2_PIN_2, OUTPUT);
  
  Serial.begin(115200);
  Serial.setDebugOutput(false);
  
  camera_config_t config;
  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG; 
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  // Wi-Fi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.println(WiFi.localIP());
  
  // Start streaming web server
  startCameraServer();
}

void loop() {
  
}

Trong đoạn code ESP32 CAM Car trên, bạn thay đổi thông tin mạng WiFi ở đoạn sau:

const char* ssid = "REPLACE_WITH_YOUR_SSID"; 
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Giải thích chương trình

Khai báo 4 chân GPIO điều khiển động cơ, mỗi động cơ được điều khiển bởi 2 chân:

#define MOTOR_1_PIN_1 14
#define MOTOR_1_PIN_2 15
#define MOTOR_2_PIN_1 13
#define MOTOR_2_PIN_2 12

Khi nhấp vào các nút trên Web Server, bạn sẽ đưa ra yêu cầu trên một URL khác:

<table>
    <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('forward');" ontouchstart="toggleCheckbox('forward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Forward</button></td></tr>
    <tr><td align="center"><button class="button" onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Left</button></td><td align="center"><button class="button" onmousedown="toggleCheckbox('stop');" ontouchstart="toggleCheckbox('stop');">Stop</button></td><td align="center"><button class="button" onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Right</button></td></tr>
    <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('backward');" ontouchstart="toggleCheckbox('backward');" onmouseup="toggleCheckbox('stop');" ontouchend="toggleCheckbox('stop');">Backward</button></td></tr>                   
</table>
<script>
    function toggleCheckbox(x) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/action?go=" + x, true);
      xhr.send();
    }
   window.onload = document.getElementById("photo").src =     window.location.href.slice(0, -1) + ":81/stream";
</script>

Dưới đây là các yêu cầu được thực hiện, tùy thuộc vào nút mà bạn nhấn trên Web Server:

  • Forward (đi tới): <ESP_IP_ADDRESS>/action?go=forward
  • Backward (đi lùi): /action?go=backward
  • Left (rẽ trái): /action?go=left
  • Right (rẽ phải): /action?go=right
  • Stop (dừng lại): /action?go=stop

Khi bạn nhả nút, một yêu cầu sẽ được thực hiện trên URL /action?go=stop. Do đó, robot chỉ di chuyển khi bạn nhấn nút.

Và dưới đây là đoạn code về các hành động của robot khi nhận được những yêu cầu trên URL đó. Mình sử dụng câu lệnh if else:

if(!strcmp(variable, "forward")) {
    Serial.println("Forward");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
 }
 else if(!strcmp(variable, "left")) {
   Serial.println("Left");
   digitalWrite(MOTOR_1_PIN_1, 0);
   digitalWrite(MOTOR_1_PIN_2, 1);
   digitalWrite(MOTOR_2_PIN_1, 1);
   digitalWrite(MOTOR_2_PIN_2, 0);
}
else if(!strcmp(variable, "right")) {
   Serial.println("Right");
   digitalWrite(MOTOR_1_PIN_1, 1);
   digitalWrite(MOTOR_1_PIN_2, 0);
   digitalWrite(MOTOR_2_PIN_1, 0);
   digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "backward")) {
   Serial.println("Backward");
   digitalWrite(MOTOR_1_PIN_1, 0);
   digitalWrite(MOTOR_1_PIN_2, 1);
   digitalWrite(MOTOR_2_PIN_1, 0);
   digitalWrite(MOTOR_2_PIN_2, 1);
}
else if(!strcmp(variable, "stop")) {
   Serial.println("Stop");
   digitalWrite(MOTOR_1_PIN_1, 0);
   digitalWrite(MOTOR_1_PIN_2, 0);
   digitalWrite(MOTOR_2_PIN_1, 0);
   digitalWrite(MOTOR_2_PIN_2, 0);
}

Chạy thử code

Sau khi thay đổi thông tin đăng nhập mạng, bạn hãy upload đoạn code ESP32 CAM Car trên vào mạch của mình.

Sau đó, bạn mở Serial Monitor để lấy địa chỉ IP của mạch ESP32.

Mở Serial Monitor để lấy địa chỉ IP ESP32, phục vụ dự án ESP32 CAM Car
Mở Serial Monitor để lấy địa chỉ IP ESP32, phục vụ dự án ESP32 CAM Car

Kế tiếp, mở trình duyệt và nhập địa chỉ IP của ESP32, một trang Web sẽ xuất hiện để bạn điều khiển ESP32 CAM Car:

Mở Web Server để điều khiển ESP32 CAM Car
Mở Web Server để điều khiển ESP32 CAM Car

Bạn hãy thử nhấn các nút trên Web Server rồi nhìn vào màn hình Serial Monitor, để xem có bị lag gì không, liệu chúng có nhận lệnh kịp thời và không gặp sự cố nào khác không.

Nếu mọi thứ hoạt động bình thường, chúng ta sẽ chuyển qua bước tiếp theo trong chế tạo robot với ESP32 – Lắp ráp mạch điện.

Lắp ráp mạch điện phần cứng

Sau khi lắp xong khung robot, bạn có thể kết nối dây trong hệ thống như sơ đồ sau:

Lắp ráp mạch điện phần cứng cho dự án ESP32 CAM Car

Đầu tiên bạn cần kết nối ESP32 CAM với L298N Motor Driver như sơ đồ. Dưới đây là chi tiết cách kết nối các cổng:

L298N Motor DriverESP32-CAM
IN1GPIO 14
IN2GPIO 15
IN3GPIO 13
IN4GPIO 12

Chúng ta có thể kết nối tất cả trên một tấm ván nhỏ như hình bên dưới, để chúng trông gọn, đẹp mắt và chắc chắn hơn:

Kết nối sơ đồ mạch cho dự án ESP32 CAM Car

Sau đó, bạn nối dây của từng động cơ vào terminal block của nó.

Ghi chú: Mình khuyên bạn nên hàn  0.1 uF ceramic capacitor vào cực dương và cực âm của mỗi động cơ, điều đó giúp làm giảm các xung điện áp. Ngoài ra, bạn có thể hàn công tắc trượt vào dây màu đỏ lấy từ sạc dự phòng (để chúng ta có thể bật và ngắt nguồn điện).

Cuối cùng, bạn cấp nguồn cho hệ thống bằng sạc dự phòng như sơ đồ kết nối ESP32 CAM Car trên. Trong dự án này, cả ESP32 và động cơ đều được cấp từ 1 nguồn và chúng hoạt động khá ổn.

Lưu ý: Động cơ tiêu thụ khá nhiều điện, nên nếu bạn thấy robot chạy không đủ nhanh hoặc không ổn định, bạn nên cân nhắc dùng nguồn điện ngoài cho động cơ. Điều này đồng nghĩa việc bạn cần 2 nguồn pin khác nhau, 1 cái cho ESP32 và 1 cái cho động cơ. Bạn có thể dùng bộ pin 4xAA để cấp nguồn cho động cơ.

Dưới đây là hình ảnh robot trong dự án ESP32 CAM Car lần này, sau khi lắp xong:

Hoàn thiện robot cho dự án  ESP32 CAM Car
Hoàn thiện robot cho dự án ESP32 CAM Car

Ngoài ra, bạn đừng quên dùng loại ESP32 CAM có gắn ăng ten ben ngoài nhé! Nếu không thì video hiển thị trên Web Server sẽ khá là giật lag đó.

Demo dự án ESP32 CAM Car

Mở trình duyệt, nhập địa chỉ IP của ESP32 CAM và điều khiển robot thôi nào! Theo mình thấy thì Web Server hoạt động ổn trên cả điện thoại lẫn laptop / PC.

Lưu ý: Bạn chỉ có thể mở 1 Web Server trên cùng 1 thiết bị tại 1 thời điểm.

Lời kết

Trên đây là các hướng dẫn chi tiết về chủ đề ESP32 CAM Car – Điều khiển robot từ xa qua Web Server, có gửi video nhận diện từ robot lên trang Web. Đây là một dự án khá thú vị phải không nào? Chúc bạn thành công nhé!

Ngoài ra, trên IoTZone cũng có một số bài viết khác về chủ đề ESP32 CAM, bạn có thể tham khảo nếu thích nhé!

IoTZone – Chuyên cung cấp thiết bị điện tử & tài liệu cho Makers

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *