【Arduino 动手做】带有摄像头的 Web 控制 ESP32 桌面机器人

在这里插入图片描述

《Arduino 手册(思路与案例)》栏目介绍:
在电子制作与智能控制的应用领域:广泛涉及了Arduino BLDC、Arduino CNC、Arduino ESP32 SPP、Arduino FreeRTOS、Arduino FOC、Arduino GRBL、Arduino HTTP、Arduino HUB75、Arduino IoT Cloud、Arduino JSON、Arduino LCD、Arduino OLED、Arduino LVGL、Arduino PID 及 Arduino TFT 等方面的相关拓展思路和众多参考案例。本专栏目前博客近2300篇。
https://blog.csdn.net/weixin_41659040/category_12422453.html

Arduino是一个开放源码的电子原型平台,它可以让你用简单的硬件和软件来创建各种互动的项目。Arduino的核心是一个微控制器板,它可以通过一系列的引脚来连接各种传感器、执行器、显示器等外部设备。Arduino的编程是基于C/C++语言的,你可以使用Arduino IDE(集成开发环境)来编写、编译和上传代码到Arduino板上。Arduino还有一个丰富的库和社区,你可以利用它们来扩展Arduino的功能和学习Arduino的知识。

Arduino的特点是:
1、开放源码:Arduino的硬件和软件都是开放源码的,你可以自由地修改、复制和分享它们。
2、易用:Arduino的硬件和软件都是为初学者和非专业人士设计的,你可以轻松地上手和使用它们。
3、便宜:Arduino的硬件和软件都是非常经济的,你可以用很低的成本来实现你的想法。
4、多样:Arduino有多种型号和版本,你可以根据你的需要和喜好来选择合适的Arduino板。
5、创新:Arduino可以让你用电子的方式来表达你的创意和想象,你可以用Arduino来制作各种有趣和有用的项目,如机器人、智能家居、艺术装置等。

在这里插入图片描述

一个简单的 Web 控制 ESP32 机器人,带有摄像头。由 LiPo 电池供电,使用 4 个 n20 电机。

在这里插入图片描述

用品与材料

1 个 ESP32-CAM 微控制器

4x N20 电机 (60RPM)(全球速卖通))

2x MX 1508 有刷直流电机驱动器(有时也称为迷你 L298 驱动器)(Amazon UK)(全球速卖通))

1x 7.4v Lipo 电池(亚马逊英国))

1 个 JST 连接器(Amazon UK)

1x Mini3060 DC-DC 降压模块(Amazon UK)(全球速卖通))

1 个 160 度 OVO2640 75mm 长的模块。(亚马逊英国)

1x 60x80mm 原型 / pef 板(我用的是双面,但单面应该可以)(全球速卖通)

一个 40 针的公板接头条。

母跳线选择。(提供 40 个每种的套件应该足够了,因为有些会被切成两半)

2 个 8 针母板接头(用于插入 esp32)

2 针摇杆开关(内部尺寸应约为 18.6 毫米 x 11.8 毫米,如果您的开关需要更大的孔,您可以将开口锉磨或打磨得更大或调整印刷品)(亚马逊英国))

12 个 M3x8 螺栓

4 个 M2x6 螺栓(如果您没有 2 个就足够了)

带有外露端的跳线,用于在配电板上创建连接

预计建造成本(2025 年 4 月):35-45 英镑/40-50 欧元/50-60 美元/60-80元人民币(但由于关税,可能会更多),但是您可能需要购买 2、5、10 等套装的某些零件,这意味着它可能会花费更多,但您有备件,甚至可能有足够的零件来建造 2 个!您可能会在其他地方找到更便宜的零件。

第 1 步:3D 打印组件

在 PLA 中打印 Wheels、bottom、mid、top、shell 部分。这总共需要大约 7-8 小时,但可能需要更长的时间,具体取决于您的打印机。我建议打印底部、车轮、轮胎和 N20 电机外壳,以便在打印外壳和上部时更快地开始项目。

使用 95A TPU 打印轮胎(确保在正确的温度下打印以减少任何瑕疵)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 2 步:焊接电机连接

您的电机可能带有预焊接的电线,但我发现它们可能很弱并且几乎不费吹灰之力就会折断。我将其中一根母对母跨接电缆切成两半,将非连接器端剥去约 5-10 毫米,然后将其焊接到电机上。

焊接到电机引脚时要小心,不要焊接得太热,否则可能会损坏电机外壳和连接点。对所有 4 个电机执行此作。我发现最好用黑色和红色电缆焊接它们,并将它们全部焊接到相同的方向(你可以通过电机上的齿轮看出)。

在这里插入图片描述
在这里插入图片描述

第 3 步:安装电机 + 轮子

在底壳上,您会看到中间有 8 个螺丝孔,这是 4 个电机外壳拧入到位的位置。

首先将每个电机插入 n20 电机外壳杆端,然后将背面卡入到位。

对于 4 个轮子将轮胎重叠在一个车轮上,它应该很紧,几乎看起来不合适,但 95a 的伸展性刚好可以放在车轮上。这样做是为了确保它保持原状。现在将每个轮子连接到 N20 电机。如果您查看 4 个部分之一的车轮,它会有一个轻微的颠簸,这是 N20 轴的平坦部分应该面向的一侧,并将车轮推到电机上。根据你的打印方式,有些可能几乎没有力量继续,但有些可能需要更多的鼓励。如果它太容易滑入和滑出,那么您可能需要检查您的打印设置。

现在拧入每个外壳,并将轮子连接到底座上。外壳应与中间部分的螺丝柱位于同一侧。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 4 步:将接头焊接到电机驱动器上

您可以直接在电机和驱动器之间焊接电线,但在尝试解决问题时可能会导致问题。我发现只需焊接一些接头就更容易了,这样我就可以连接或断开跳线以帮助发现任何问题。焊接所有 9 个接头。您应该能够为每个 2 针孔一组 2 个来执行它们。同样,不要焊接得太热,否则可能会熔化将引脚固定到位的塑料。

在这里插入图片描述

第 5 步:焊接配电板

图为配电板上需要如何连接。我的焊接不是最好的,但应该让您大致了解它们应该如何协同工作。lipo 需要为电机驱动器的 2 个电源连接和直流降压转换器的连接供电。转换器的另一侧需要连接到 ESP32 上的 5V 和 GND。然后将 8 个接头(2 行,每行 4 行)连接到 GPIO。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 6 步:连接所有内容

一旦打印出中间部分,您就可以将每个电机的电缆向上穿过提供的小孔,如图所示插入电机驱动器,并将电池放在中间的部分…确保您也拧紧了中段。拧紧电源板,使连接点朝上。将电源和接地连接到每个电机控制器,然后连接到每根控制电缆。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 7 步:对 ESP32 CAM 进行编程

将代码上传到开发板,但将您的 SSID 名称和密码插入相关部分。加载后,使用 115200 波特率的串行监视器查看设备将使用的 IP。(如果未在路由器中分配静态 IP,这可能会发生变化,但您也可以通过查看连接的设备来检查路由器上的 IP)

在这里插入图片描述
在这里插入图片描述

//Adapted Code based on project by NT Tronix https://nabatechblog.com/esp32-cam-wifi-camera-with-pan-tilt-control-diy-smart-surveillance/
#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"
#include "soc/rtc_cntl_reg.h"
#include "esp_http_server.h"

// Replace with your network credentials
const char* ssid = "INSERTSSIDHERE";
const char* password = "INSERTPASSWORDHERE";

#define PART_BOUNDARY "123456789000000000000987654321"

#define CAMERA_MODEL_AI_THINKER

#if 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
#else
  #error "Camera model not selected"
#endif

// Motor control pins
#define MOTOR_LEFT_IN1   14
#define MOTOR_LEFT_IN2   15
#define MOTOR_RIGHT_IN1  12
#define MOTOR_RIGHT_IN2  13

// Streaming constants
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;

// Simple web UI
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
  <head><title>ESP32-CAM Remote Robot</title>
  <style>
    body { font-family: Arial; text-align: center; }
    .button {
      background-color: #2f4468; border: none; color: white;
      padding: 10px 20px; font-size: 18px; margin: 6px 3px;
      cursor: pointer; touch-action: manipulation;
    }
  </style>
  </head>
  <body>
    <h1>ESP32-CAM Motor Control</h1>
    <img src="" id="photo">
     <div>
      <button class="button" ontouchstart="send('up')" ontouchend="send('stop')"
              onmousedown="send('up')" onmouseup="send('stop')">Up</button><br>
      <button class="button" ontouchstart="send('left')" ontouchend="send('stop')"
              onmousedown="send('left')" onmouseup="send('stop')">Left</button>
      <button class="button" ontouchstart="send('stop')" ontouchend="send('stop')"
              onmousedown="send('stop')" onmouseup="send('stop')">Stop</button><br>
      <button class="button" ontouchstart="send('right')" ontouchend="send('stop')"
              onmousedown="send('right')" onmouseup="send('stop')">Right</button><br>
      <button class="button" ontouchstart="send('down')" ontouchend="send('stop')"
              onmousedown="send('down')" onmouseup="send('stop')">Down</button>
    </div>
    <script>
      function send(cmd) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/action?go=" + cmd, true);
        xhr.send();
      }
      window.onload = () => document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
    </script>
  </body>
</html>
)rawliteral";

// Motor control helper
void stopMotors() {
  digitalWrite(MOTOR_LEFT_IN1, LOW);
  digitalWrite(MOTOR_LEFT_IN2, LOW);
  digitalWrite(MOTOR_RIGHT_IN1, LOW);
  digitalWrite(MOTOR_RIGHT_IN2, LOW);
}

void moveBackward() {
  digitalWrite(MOTOR_LEFT_IN1, HIGH);
  digitalWrite(MOTOR_LEFT_IN2, LOW);
  digitalWrite(MOTOR_RIGHT_IN1, HIGH);
  digitalWrite(MOTOR_RIGHT_IN2, LOW);
}

void moveForward() {
  digitalWrite(MOTOR_LEFT_IN1, LOW);
  digitalWrite(MOTOR_LEFT_IN2, HIGH);
  digitalWrite(MOTOR_RIGHT_IN1, LOW);
  digitalWrite(MOTOR_RIGHT_IN2, HIGH);
}

void turnRight() {
  digitalWrite(MOTOR_LEFT_IN1, LOW);
  digitalWrite(MOTOR_LEFT_IN2, HIGH);
  digitalWrite(MOTOR_RIGHT_IN1, HIGH);
  digitalWrite(MOTOR_RIGHT_IN2, LOW);
}

void turnLeft() {
  digitalWrite(MOTOR_LEFT_IN1, HIGH);
  digitalWrite(MOTOR_LEFT_IN2, LOW);
  digitalWrite(MOTOR_RIGHT_IN1, LOW);
  digitalWrite(MOTOR_RIGHT_IN2, HIGH);
}

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->format != PIXFORMAT_JPEG){
        bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
        esp_camera_fb_return(fb);
        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);
    }
    if(res != ESP_OK){
      break;
    }
  }
  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) {
      httpd_query_key_value(buf, "go", variable, sizeof(variable));
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  if(!strcmp(variable, "up")) {
    moveForward();
  } else if(!strcmp(variable, "down")) {
    moveBackward();
  } else if(!strcmp(variable, "left")) {
    turnLeft();
  } else if(!strcmp(variable, "right")) {
    turnRight();
  } else if(!strcmp(variable, "stop")) {
    stopMotors();
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  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 };
  httpd_uri_t cmd_uri = { .uri = "/action", .method = HTTP_GET, .handler = cmd_handler };
  httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler };

  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);
  Serial.begin(115200);

  // Motor pins
  pinMode(MOTOR_LEFT_IN1, OUTPUT);
  pinMode(MOTOR_LEFT_IN2, OUTPUT);
  pinMode(MOTOR_RIGHT_IN1, OUTPUT);
  pinMode(MOTOR_RIGHT_IN2, OUTPUT);
  stopMotors();
  WiFi.hostname("ESP32-MiniBot");  
  // Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");

  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_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;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;

  if (esp_camera_init(&config) != ESP_OK) {
    Serial.println("Camera init failed");
    return;
  }

  Serial.println("Camera ready at:");
  Serial.println(WiFi.localIP());
  Serial.printf("PSRAM size: %d bytes\n", ESP.getPsramSize());
  Serial.printf("Free PSRAM: %d bytes\n", ESP.getFreePsram());
  startCameraServer();
}

void loop() {}

第 8 步:测试它有效

在下一步切割电池连接器之前,将电池连接到电源板上,电缆完好无损,并测试 esp32 是否上电。

第 9 步:添加 Power 按钮并关闭 shell

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第 10 步:控制机器人

打开电源开关,然后在您的 PC、Mac 或 Phone 上,通过步骤 7 中所述的 IP 地址连接到浏览器中的设备。(您的机器人本地 IP 可能与我的不同)。您应该会看到一个摄像头源和 4 个按钮来控制。在移动设备上,按钮有时会卡住,因此在台式机/笔记本电脑浏览器上通常控制得更好。那应该是它,如果您发现您的电机没有正确运行,那么您可能需要检查您的接线并重新组织它,直到电机都正常工作。

为了给电池充电,我还没有在这个项目中加入 lipo 充电器,所以你需要拧下顶部,断开电池并将其连接到充电器(通常随电池一起提供)。但是,它可能是您融入自己的升级点!

在这里插入图片描述

第 11 步:未来版本

正如您会注意到的那样,在摄像头正下方的外壳中,有一个足够大的空间可以容纳 15 毫米扬声器,而在中段框架上,则有空间可以连接MAX98357A。将来,我计划更新项目以包含此内容,但请随时自行添加!esp32 上应该有足够的引脚供您添加它。只需调整配电板即可从 ESP32-CAM 进行连接。

我计划在今年晚些时候发布这个机器人的第 2 版,希望它能包括音频,甚至可能还包括麦克风功能,这样它就可以用作远程控制安全摄像头。

附录
项目链接:https://www.instructables.com/ESP32-CAM-Web-Controlled-Car/
项目作者:英国 Kent(BeepBoopPrints)

项目代码:https://github.com/BeepBoopindie/ESP32CAMBOT/blob/main/ESP32CAMBOT.ino
3D打印文件:https://content.instructables.com/FKL/O6ST/M9PNR6FM/FKLO6STM9PNR6FM._3mf

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

驴友花雕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值