Table of Contents
Today we are going to build a robot car using the popular ESP32CAM module. This is an amusing and educational project for builders of all ages!
Our car will have a web-based control interface, so you can drive it and view the camera from any computer, tablet, or phone.
Introduction
We have already taken a look at the amazing ESP32CAM module. This little 9-dollar wonder has an ESP32S module with a camera and microSD card, as well as connections to several ESP32 GPIO pins.
We are going to use this module as the basis for a simple robot car. We will make use of the ESP32CAMs built-in WiFi capabilities to program a web-based user interface that will allow us to drive the car and to view the video camera output.
You can use this design as the heart of a more sophisticated robot, taking advantage of the many functions of the ESP32 to add sensors and output devices.
This robot would also be an excellent project for students or someone teaching electronics to a beginner, as the resulting product is interesting and fun to use and can be expanded and extended in many different ways. Plus there are a lot of fundamental concepts like assembly, soldering, and programming that are involved in building the robot.
Whatever the reason you have for making it you’ll find it to be a fun and entertaining project. So let’s get started!
ESP32CAM Robot Car Parts
I’m going to show you how I built this car, but you don’t need to follow my assembly techniques, although you are certainly most welcome to do so. The real beauty of a project like this is that you can use parts you already have and come up with your own assembly technique. So your robot car won’t necessarily look like my robot car, in fact, it could even be a robot tank, truck, or another vehicle!
However, there are some basic components that all designs will require. Let’s take a look at these essential electronic components.
ESP32CAM Module
Obviously, you will need an ESP32CAM module, as this is the component upon which our design is based. This is a very inexpensive module available at many distributors, including Amazon and eBay.
The module is often shipped with the camera unattached, but it is quite a simple matter to install it.
FTDI Adapter
The FTDI (Future Technology Devices International) adapter is a device that allows you to connect a USB port to a microcontroller. These are common devices, and you should have at least one around in your workshop as several microcontrollers require them.
You don’t need to have the adapter permanently attached to the robot, it is just needed to program the ESP32CAM.
Your FTDI adapter should have either a jumper or solder pads that allow it to be set to either 3.3-volt or 5-volt logic levels. For this project you’ll want to ensure that you use 3.3-volt logic levels.
Motor Driver
Our car will use (at least) two motorized wheels, and those motors will need a motor driver or “H-Bridge” interface to supply ample power and current at the proper polarity.
For this project, we will be using the TB6612FNG H-Bridge controller, which we have worked with before. Unlike the popular L298N H-Bridge, the TB6612FNG uses MOSFETs to allow for greater current handling and less heat dissipation. The TB6612FNG is also more efficient, providing virtually no voltage drop as opposed to the 1.4-volts you lose when using an L298N.
These modules are inexpensive and available at many electronics vendors. They usually don’t come with the pins soldered, so you’ll need to install them.
Power Supply
Of course, our robot car isn’t going anywhere without a source of power!
You’ll need to supply two voltages for the car:
- Power for the ESP32CAM – Although the module works on 3.3-volts it also has a 5-volt power input, and many experimenters have discovered that this is more reliable than the 3.3-volt one. So I suggest that you allow for 5-volts, with a decent amount of current capability as the WiFi connection requires occasional bursts of power.
- Power for the Motors – This voltage level will depend upon your motors of course. The main factors here are that it (a) needs to be independent from the ESP32CAM power supply and (b) needs to have sufficient current to power the motors.
For my robot, I designed a small power supply using an adjustable buck converter and powered by a pair of 18650 batteries. The buck converter supplies the voltage for the ESP32CAM, and the motors are driven right from the batteries (7.4-volts total).
You can also use another supply, or just use a 5-volt power bank for the ESP32CAM supply and a bunch of AA batteries for the motor.
Antenna
This one is optional, as the ESP32CAM module has an integrated antenna. But for good performance, you are best to use an external antenna.
In our design the ESP32CAM module will act as an “access point”, meaning it will set up its own WiFi network. Your device (phone, tablet, or computer) wil need to connect to the robot’s WiFi signal, so you’ll want it as strong as you can get it.
With the external antenna, I am able to control the robot on the second floor of my house while I am in my basement! I haven’t had the opportunity to test it outside yet, but I’m sure the range will be good.
Some Design Considerations
I built my robot car using a standard acrylic base, I’ve used these kits before in several designs. They come complete with wheels, motors, a castor and all mounting hardware.
You could use a similar kit, or you could build your car from scratch (which is even more creative). You might also want to modify an existing model car or truck to create your ESP32CAM car.
No matter how you design your robot car there are some design considerations that are common to every build. Let’s look at a few of them now.
ESP32CAM Limitations
One of the major considerations I had when designing this robot was that the ESP32CAM module does have quite a few limitations, at least when compared to a “regular” ESP32.
- Fewer GPIO Pins – Many of the ESP32 GPIO pins are used for the camera module, so they are not available for use externally.
- Shared GPIO Pins – Some of the ESP32CAM pins have multiple functions. GPIO pin 0 is grounded when you want to put the ESP32CAM into programming mode, and GPIO pins 1 and 3 are used for receive and transmit and are used with the FTDI adapter.
Now, this doesn’t mean that you can’t use these pins, it just means you will have to be aware of their other uses and make certain you don’t interfere with them. If you use GPIO 1 or 3 then you’ll also have to realize that you won’t be able to use the serial monitor during development.
- Shared Resources – Like other microcontrollers, the ESP32 has a number of registers and timers that are used for a variety of purposes. Some of these resources are used by the camera and microSD card, so if you use a library that also makes use of the same resource you’ll have a conflict.
In the robot car I built, some of the resources I used for the motor driver conflict with the resources needed for the microSD card. The MicroSD card is not used in this design, so that’s OK. It’s an example of the tradeoffs you need to make sometimes.
Camera Considerations
Obviously one of the key features of this project is the camera. The ESP32CAM module comes with a 2.1 fixed-focus megapixel camera module, which will allow us to get a “cars-eye-view” of the action, enabling you to actually navigate the car.
A couple of considerations here:
- Camera Quality – While the picture is certainly acceptable it is by no means HD quality, but for this application it really doesn’t have to be. I have also found a large discrepancy between different ESP32CAM boards, if you have a few you might want to see which one looks best to you.
- Camera Mounting – The camera is affixed to the microSD card reader on the ESP32CAM, and without a longer cable that’s about the only position you can hold it in. This makes mounting and orienting the camera a bit more of a challenge than it would be if the camera had a longer cable.
Not much we can do about camera quality except adjust it, the sample ESP32CAM code has a number of video parameter adjustments that we can include in our code. I only exposed the video size and resolution ones, but you could modify the code to add more.
One thing you cannot do, however, is to substitute a higher-resolution camera, like a 5 megapixel one. The way that the ESP32CAM module works prevents this from being done.
As for mounting, I solved that by mounting the ESP32CAM module on a separate board, which plugs into my main board. This keeps the camera oriented correctly. If you’re using solderless breadboards.
Antenna Mounting
While you could use the internal antenna you are really advised to add an external antenna to the ESP32CAM module. I know I’ve already mentioned that, but I felt it needed to be said again!
You will need to mount your external antenna on the robot, in a location close to the ESP32CAM as most of these antennas have relatively short cables. And you’ll also want to orient it correctly, for best performance.
I mounted my antenna on a separate piece of perfboard, which I mounted above the main board using some tall spacers. I also used the same board to hold the power switch and a power indicator LED.
I used a common dipole antenna, but you can also get stick-on antennas as well. Although I haven’t tried one I suspect the stick-on ones wouldn’t perform as well.
Robot Car Hookup
So now that we have all of the components for our ESP32CAM robot car together it’s time to start wiring them up. Before we get started, however, we need to figure out how you’re going to hook everything up.
Construction methods
You have a few choices when it comes to hooking everything up, ultimately the correct choice will be determined by the answers to a few questions.
- Is this going to be a permanent project or just an experiment, where you will eventually reuse the components for another project?
- Do you have experience soldering, as well as equipment to do the job?
- Does your car body (if you’re not using the robot car chassis I used) have specific space requirements for circuit boards?
Based upon your responses to those questions you can determine the most suitable construction technique for your robot car.
Perfboards
This is the method that I used to construct my robot car, and it’s my preferred method of wiring up any electronics project.
Perfboard (perforated experimenters breadboards) are a great prototyping method, especially if you eventually intend to develop a printed circuit board for your project. You can move components around on a perfboard and get the positioning correct, and perfboard is also easier to modify if you find your original wiring had a design error.
I use two gauges of wire when working with perfboard:
- Data and Signal Connections – I use 30 gauge “wire-wrap” wire for these connections. Despite its small size it is very easy to work with and solder.
- Power and Ground Connections – I use 22 gauge “hook-up” wire for these connections. In this design I also used 22 gauge wire for the connections to the motor terminals. While the thicker wire is a bit harder to work with it has a lower resistance, so I use it for grounds and any current-carrying lines.
When you purchase perfboards look for the better-quality stuff, the boards with plated-thru holes. The cheaper stuff is hard to solder and warps easily.
This would be my recommended method of assembling this project.
Solderless Breadboards
If you are only building this robot car as an experiment or proof-of-concept then you might consider using solderless breadboards. Note that I’m using the plural “breadboards” here, as you’ll likely need at least two of them, as the ESP32CAM module will need to be mounted vertically in order to use the camera.
This has the advantage of being a very quick way to get all of the wiring done.
The biggest disadvantage of using a solderless breadboard is reliability. Remember, the robot car is a moving object, subject to crashes and vibrations. All of this rough treatment could easily cause intermittent connections.
Another potential problem is that connecting the external antennal to the ESP32CAM module will be a bit awkward. And, on the subject of the ESP32CAM module, it needs to be mounted so that you can get access to the reset button, which is on the underside of the module, and that’s also a bit tricky with a solderless breadboard.
By the way, if your only reason for choosing a solderless breadboard is to avoid soldering then you’re probably out of luck. Most of those “robot car” chassis kits don’t have the motor wires soldered, so you’ll need to solder them. And the TB6612FNG H-Bridge module is usually sold without the pins soldered, so they will need soldering as well.
“Hybrid” Construction
I used the word “hybrid” for lack of a better term.
Essentially with this method you will need to solder everything, but you don’t need to use the tiny wire-wrap wire, which some people may find challenging to work with (although I actually find it very easy to use).
To build the car in a “hybrid” fashion you can use a few pieces of “stripboard” or you can use pre-made circuit boards that replace solderless breadboards.
A stripboard is a printed circuit board that has strips of copper on it. You can break these strips using a special tool, or just use the tip of a drill bit. This allows you to make electrical connections through the strips, or to use jumper wires to route signals as well.
The printed circuit boards that look like solderless breadboards are used to create permanent versions of circuits after you breadboard them. Instead of using tiny wire wrap-wires you only need to work with standard size jumper wires, which some people may find a lot easier.
FTDI Hookup
We will begin our robot car hookup by connecting the FTDI adapter to the ESP32CAM module. You will need this adapter to load the code that you create for the car into the ESP32CAM.
The FTDI adapter does not need to be permanently attached, as it is only used during the programming phase of the project. In my design I provided a female socket for the adapter, so that could remove it when not in use.
The wiring is very simple, essentially all we are doing is connecting the transmit and receive pins on the ESP32 to the receive and transmit pins on the FTDI adapter (i.e. ESP32CAM transmit connects to FTDI receive, and vice-versa).
There are also a pair of jumpers, they have the following purpose:
- PWR – This jumper allows you to power the ESP32CAM module from the FTDI adapter, instead of using the robot car power supply. You might want to do this during programming, especially if you built the ESP32CAM module to be removable as I did. It allows you to avoid connecting the robot car power supply and FTDI adapter supply directly together.
- PGM – To put the ESP32CAM module into programming mode it is necessary to tie GPIO pin 0 to ground. This jumper accomplishes this. It must be inserted during programming, and removed during normal operation.
Make sure that you set your FTDI adapter for 3.3-volt logic, this is usually accomplished either with a jumper or solder-pad.
Motor Controller Hookup
The next section we will hook up is the motor controller. Here is the wiring diagram:
You’ll notice that the TB6612FNG motor controller has all of its control inputs on one side of the module, and all of the power and motor connections on the other side. This will simplify your wiring task.
You will also note that both the PWMA and PWMB inputs are connected together and driven by a single GPIO output, on pin GPIO pin 12. This hookup saves one GPIO pin, at the expense of making it impossible to control the motor speeds independently. I felt this was an adequate trade-off.
Make sure not to forget to connect the STBY input to the VCC, this is the ”standby” input and must be held HIGH for the motor control to be enabled.
Also note the arrangement of the motor outputs, they “mirror” each other” The motor control inputs on the other side of the module are also “mirrored” in a similar fashion.
Power Supply
Finally, we need to build our power supply. If you want to build the one I used then here is the hookup diagram:
I’m using a pair of 18650 LiPo cells in my supply, each cell provides an output of 3.7-volts for a total of 7.4-volts. The 7.4-volts is used to power the motors directly.
I also have a DC-DC converter, or buck converter, to supply 5-volts for the ESP32CAM. Although the hESP32CAM is actually a 3.3-volt module it has dual power inputs,m and many experimenters report better results using the 5-volt one.
The DC-DC converter I’m using is a popular one based around the LM2596 regulator. As this is an adjustable converter it must be adjusted for a 5-volt output using the multi-turn potentiometer on the module.
Make sure you set the output for 5-volts BEFORE powering up the ESP32CAM module!
You could also use a different regulator, one that is fixed at a 5-volt output would eliminate the need to perform any adjustments.
Otherwise, the only other components in the power supply I used were an SPST power switch and an LED power indicator. I used a “rainbow LED (Pimoroni COM0625), but any standard LED would suffice.
Robot Car Assembly
Now that we have our circuit boards (or breadboards) wired up it’s time to put the car together.
Naturally, if you have chosen to build the car using an old toy, or your own unique design, then the assembly process will be different for you.
If you are using a robot car base similar to the one I used then it likely came in a kit with the acrylic base, the two motors, a castor, and some mounting hardware. The motors may or may not have wires soldered to them.
The acrylic base is usually packaged with a brown paper cover, which should be removed before you assemble everything. While this cover is really meant to protect the acrylic during shipping it also is ideal for marking off any of the holes you’ll need to drill to mount your circuit boards.
Here are a few images of the robot car I built, to give you an idea as to how I assembled mine.
However, once I had mine assembled I noticed that my design was very “front-heavy”, and the car would lift up its back end every time I stopped moving!
I resolved this by adding a second castor assembly at the front of the car. Probably not the only way to fix the issue, moving the battery and circuit boards would probably have been better, but it did work.
After you get it all together do a quick double-check of your wiring. Then we can move on to the code.
Robot Car Code
The code for our robot car is in two files:
- esp32cam-robot.ino – This is the main file, which initializes the ESP32 and sets up the camera.
- app_httpd.cpp – This is the file that builds the web-based interface.
I certainly can’t take credit for most of the code, as the majority of it has been copied from the sample application that Espressif provides for working with the ESP32CAM module. I have simply used sections of their code and added controls for manipulating the motor and the onboard flash LED.
Instead of examining the code line-for-line, we can just look at the sections that we might want to modify, to create our own unique remote control or top add features to the robot car.
esp32cam-robot.ino
Here is the main file, the esp32cam-robot.ino file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
/* ESP32CAM Robot Car esp32cam-robot.ino (requires app_httpd.cpp) Based upon Espressif ESP32CAM Examples Uses TBA6612FNG H-Bridge Controller DroneBot Workshop 2021 https://dronebotworkshop.com */ #include "esp_wifi.h" #include "esp_camera.h" #include <WiFi.h> #include "soc/soc.h" #include "soc/rtc_cntl_reg.h" // Setup Access Point Credentials const char* ssid1 = "ESP32-CAM Robot"; const char* password1 = "1234567890"; extern volatile unsigned int motor_speed; extern void robot_stop(); extern void robot_setup(); extern uint8_t robo; extern volatile unsigned long previous_time; extern volatile unsigned long move_interval; #define 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 void startCameraServer(); void setup() { WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // prevent brownouts by silencing them Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); 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; //init with high specs to pre-allocate larger buffers if(psramFound()){ config.frame_size = FRAMESIZE_QVGA; config.jpeg_quality = 10; config.fb_count = 2; } else { config.frame_size = FRAMESIZE_QVGA; 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; } //drop down frame size for higher initial frame rate sensor_t * s = esp_camera_sensor_get(); s->set_framesize(s, FRAMESIZE_QVGA); s->set_vflip(s, 1); s->set_hmirror(s, 1); WiFi.softAP(ssid1, password1); IPAddress myIP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(myIP); startCameraServer(); ledcSetup(7, 5000, 8); ledcAttachPin(4, 7); //pin4 is LED robot_setup(); for (int i=0;i<5;i++) { ledcWrite(7,10); // flash led delay(50); ledcWrite(7,0); delay(50); } previous_time = millis(); } void loop() { if(robo) { unsigned long currentMillis = millis(); if (currentMillis - previous_time >= move_interval) { previous_time = currentMillis; robot_stop(); char rsp[32]; sprintf(rsp,"SPPED: %d",motor_speed); Serial.println("Stop"); robo=0; } } delay(1); yield(); } |
This file sets up the parameters for the ESP32 “AI_THINKER” module, which is the module used on the ESP32CAM.
Lines 18 & 19 are the only two bits of code that you might want to change, they set up the WiFi Access Point credentials. As they are the credentials are as follows:
- SSID (Network Name) – ESP32-CAM Robot
- Password – 1234567890
Unless you really want different credentials you can leave this file as-is. If you were to build multiple robots you might want to use a different SSID for each one. And if you are concerned about your neighbors hijacking your car you may want to use a more secure password!
Otherwise, you may leave this file alone.
app_httpd.cpp
This file contains the HTML, JavaScript and CSS used to create the user interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 |
/* ESP32CAM Robot Car app_httpd.cpp (requires esp32cam-robot.ino) Based upon Espressif ESP32CAM Examples Uses TBA6612FNG H-Bridge Controller DroneBot Workshop 2021 https://dronebotworkshop.com */ #include "dl_lib_matrix3d.h" #include <esp32-hal-ledc.h> #include "esp_http_server.h" #include "esp_timer.h" #include "esp_camera.h" #include "img_converters.h" #include "Arduino.h" // TB6612FNG H-Bridge Connections (both PWM inputs driven by GPIO 12) #define MTR_PWM 12 #define LEFT_M0 15 #define LEFT_M1 14 #define RIGHT_M0 13 #define RIGHT_M1 2 // Define Speed variables int speed = 255; int noStop = 0; //Setting Motor PWM properties const int freq = 2000; const int motorPWMChannnel = 8; const int lresolution = 8; volatile unsigned int motor_speed = 200; volatile unsigned long previous_time = 0; volatile unsigned long move_interval = 250; // Placeholder for functions void robot_setup(); void robot_stop(); void robot_fwd(); void robot_back(); void robot_left(); void robot_right(); uint8_t robo = 0; typedef struct { httpd_req_t *req; size_t len; } jpg_chunking_t; #define PART_BOUNDARY "123456789000000000000987654321" 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 stream_httpd = NULL; httpd_handle_t camera_httpd = NULL; static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len) { jpg_chunking_t *j = (jpg_chunking_t *)arg; if (!index) { j->len = 0; } if (httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK) { return 0; } j->len += len; return len; } static esp_err_t capture_handler(httpd_req_t *req) { camera_fb_t * fb = NULL; esp_err_t res = ESP_OK; int64_t fr_start = esp_timer_get_time(); fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); httpd_resp_send_500(req); return ESP_FAIL; } httpd_resp_set_type(req, "image/jpeg"); httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); size_t out_len, out_width, out_height; uint8_t * out_buf; bool s; { size_t fb_len = 0; if (fb->format == PIXFORMAT_JPEG) { fb_len = fb->len; res = httpd_resp_send(req, (const char *)fb->buf, fb->len); } else { jpg_chunking_t jchunk = {req, 0}; res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk) ? ESP_OK : ESP_FAIL; httpd_resp_send_chunk(req, NULL, 0); fb_len = jchunk.len; } esp_camera_fb_return(fb); int64_t fr_end = esp_timer_get_time(); Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start) / 1000)); return res; } dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); if (!image_matrix) { esp_camera_fb_return(fb); Serial.println("dl_matrix3du_alloc failed"); httpd_resp_send_500(req); return ESP_FAIL; } out_buf = image_matrix->item; out_len = fb->width * fb->height * 3; out_width = fb->width; out_height = fb->height; s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf); esp_camera_fb_return(fb); if (!s) { dl_matrix3du_free(image_matrix); Serial.println("to rgb888 failed"); httpd_resp_send_500(req); return ESP_FAIL; } jpg_chunking_t jchunk = {req, 0}; s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk); dl_matrix3du_free(image_matrix); if (!s) { Serial.println("JPEG compression failed"); return ESP_FAIL; } int64_t fr_end = esp_timer_get_time(); return res; } 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]; dl_matrix3du_t *image_matrix = NULL; static int64_t last_frame = 0; if (!last_frame) { last_frame = esp_timer_get_time(); } 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); 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; } int64_t fr_end = esp_timer_get_time(); int64_t frame_time = fr_end - last_frame; last_frame = fr_end; frame_time /= 1000; Serial.printf("MJPG: %uB %ums (%.1ffps)\n", (uint32_t)(_jpg_buf_len), (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time ); } last_frame = 0; return res; } enum state {fwd, rev, stp}; state actstate = stp; static esp_err_t cmd_handler(httpd_req_t *req) { char* buf; size_t buf_len; char variable[32] = {0,}; char value[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, "var", variable, sizeof(variable)) == ESP_OK && httpd_query_key_value(buf, "val", value, sizeof(value)) == 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; } int val = atoi(value); sensor_t * s = esp_camera_sensor_get(); int res = 0; // Look at values within URL to determine function if (!strcmp(variable, "framesize")) { Serial.println("framesize"); if (s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val); } else if (!strcmp(variable, "quality")) { Serial.println("quality"); res = s->set_quality(s, val); } else if (!strcmp(variable, "flash")) { ledcWrite(7, val); } else if (!strcmp(variable, "flashoff")) { ledcWrite(7, val); } else if (!strcmp(variable, "speed")) { if (val > 255) val = 255; else if (val < 0) val = 0; speed = val; ledcWrite(8, speed); } else if (!strcmp(variable, "nostop")) { noStop = val; } else if (!strcmp(variable, "car")) { if (val == 1) { Serial.println("Forward"); robot_fwd(); robo = 1; } else if (val == 2) { Serial.println("TurnLeft"); robot_left(); robo = 1; } else if (val == 3) { Serial.println("Stop"); robot_stop(); } else if (val == 4) { Serial.println("TurnRight"); robot_right(); robo = 1; } else if (val == 5) { Serial.println("Backward"); robot_back(); robo = 1; } if (noStop != 1) { } } else { Serial.println("variable"); 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); } static esp_err_t status_handler(httpd_req_t *req) { static char json_response[1024]; sensor_t * s = esp_camera_sensor_get(); char * p = json_response; *p++ = '{'; p += sprintf(p, "\"framesize\":%u,", s->status.framesize); p += sprintf(p, "\"quality\":%u,", s->status.quality); *p++ = '}'; *p++ = 0; httpd_resp_set_type(req, "application/json"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); return httpd_resp_send(req, json_response, strlen(json_response)); } static const char PROGMEM INDEX_HTML[] = R"rawliteral( <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>ESP32 CAM Robot</title> <style> body{font-family:Arial,Helvetica,sans-serif;background:#181818;color:#efefef;font-size:16px}h2{font-size:18px}section.main{display:flex}#menu,section.main{flex-direction:column}#menu{display:none;flex-wrap:nowrap;min-width:340px;background:#363636;padding:8px;border-radius:4px;margin-top:-10px;margin-right:10px}#content{display:flex;flex-wrap:wrap;align-items:stretch}figure{padding:0;margin:0;-webkit-margin-before:0;margin-block-start:0;-webkit-margin-after:0;margin-block-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:0;margin-inline-end:0}figure img{display:block;width:100%;height:auto;border-radius:4px;margin-top:8px}@media (min-width:800px) and (orientation:landscape){#content{display:flex;flex-wrap:nowrap;align-items:stretch}figure img{display:block;max-width:100%;max-height:calc(100vh - 40px);width:auto;height:auto}figure{padding:0;margin:0;-webkit-margin-before:0;margin-block-start:0;-webkit-margin-after:0;margin-block-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:0;margin-inline-end:0}}section #buttons{display:flex;flex-wrap:nowrap;justify-content:space-between}#nav-toggle{cursor:pointer;display:block}#nav-toggle-cb{outline:0;opacity:0;width:0;height:0}#nav-toggle-cb:checked+#menu{display:flex}.input-group{display:flex;flex-wrap:nowrap;line-height:22px;margin:5px 0}.input-group>label{display:inline-block;padding-right:10px;min-width:47%}.input-group input,.input-group select{flex-grow:1}.range-max,.range-min{display:inline-block;padding:0 5px}button{display:block;margin:5px;padding:5px 12px;border:0;line-height:28px;cursor:pointer;color:#fff;background:#035806;border-radius:5px;font-size:16px;outline:0;width:100px}.button2{background-color:#008cba;width:100px}.button3{background-color:#f44336;width:100px}.button4{background-color:#e7e7e7;color:#000;width:120px}.button5{background-color:#555;width:100px}.button6{visibility:hidden;width:100px}button:hover{background:#ff494d}button:active{background:#f21c21}button.disabled{cursor:default;background:#a0a0a0}input[type=range]{-webkit-appearance:none;width:100%;height:22px;background:#363636;cursor:pointer;margin:0}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{width:100%;height:2px;cursor:pointer;background:#efefef;border-radius:0;border:0 solid #efefef}input[type=range]::-webkit-slider-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer;-webkit-appearance:none;margin-top:-11.5px}input[type=range]:focus::-webkit-slider-runnable-track{background:#efefef}input[type=range]::-moz-range-track{width:100%;height:2px;cursor:pointer;background:#efefef;border-radius:0;border:0 solid #efefef}input[type=range]::-moz-range-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer}input[type=range]::-ms-track{width:100%;height:2px;cursor:pointer;background:0 0;border-color:transparent;color:transparent}input[type=range]::-ms-fill-lower{background:#efefef;border:0 solid #efefef;border-radius:0}input[type=range]::-ms-fill-upper{background:#efefef;border:0 solid #efefef;border-radius:0}input[type=range]::-ms-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer;height:2px}input[type=range]:focus::-ms-fill-lower{background:#efefef}input[type=range]:focus::-ms-fill-upper{background:#363636}.switch{display:block;position:relative;line-height:22px;font-size:16px;height:22px}.switch input{outline:0;opacity:0;width:0;height:0}.slider{width:50px;height:22px;border-radius:22px;cursor:pointer;background-color:grey}.slider,.slider:before{display:inline-block;transition:.4s}.slider:before{position:relative;content:"";border-radius:50%;height:16px;width:16px;left:4px;top:3px;background-color:#fff}input:checked+.slider{background-color:#ff3034}input:checked+.slider:before{-webkit-transform:translateX(26px);transform:translateX(26px)}select{border:1px solid #363636;font-size:14px;height:22px;outline:0;border-radius:5px}.image-container{position:absolute;top:50px;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:160px}.control-container{position:relative;top:400px;left:50%;margin-right:-50%;transform:translate(-50%,-50%)}.slider-container{position:relative;top:750px;right:36%;margin-left:-50%;transform:translate(-50%,-50%)}.close{position:absolute;right:5px;top:5px;background:#ff3034;width:16px;height:16px;border-radius:100px;color:#fff;text-align:center;line-height:18px;cursor:pointer}.hidden{display:none}.rotate90{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-o-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)} </style> </head> <body> <br> <br> <section class="main"> <figure> <div id="stream-container" class="image-container"> <div class="close" id="close-stream">×</div> <img id="stream" src="" class="rotate90"> </div> </figure> <br> <br> <section id="buttons"> <div id="controls" class="control-container"> <table> <tr><td align="center"><button class="button button6" id="get-still">Image</button></td><td align="center"><button id="toggle-stream">Start</button></td><td></td></tr> <tr><td></td><td align="center"><button class="button button2" id="forward" onclick="fetch(document.location.origin+'/control?var=car&val=1');">FORWARD</button></td><td></td></tr> <tr><td align="center"><button class="button button2" id="turnleft" onclick="fetch(document.location.origin+'/control?var=car&val=2');">LEFT</button></td><td align="center"></td><td align="center"><button class="button button2" id="turnright" onclick="fetch(document.location.origin+'/control?var=car&val=4');">RIGHT</button></td></tr> <tr><td></td><td align="center"><button class="button button2" id="backward" onclick="fetch(document.location.origin+'/control?var=car&val=5');">REVERSE</button></td><td></td></tr> <tr><td></td><td align="center"><button class="button button4" id="flash" onclick="fetch(document.location.origin+'/control?var=flash&val=1');">LIGHT ON</button></td><td></td></tr> <tr><td></td><td align="center"><button class="button button4" id="flashoff" onclick="fetch(document.location.origin+'/control?var=flashoff&val=0');">LIGHT OFF</button></td><td></td></tr> </table> </div> <br> <div id="sliders" class="slider-container"> <table> <tr><td>Motor Speed:</td><td align="center" colspan="2"><input type="range" id="speed" min="0" max="255" value="200" onchange="try{fetch(document.location.origin+'/control?var=speed&val='+this.value);}catch(e){}"></td></tr> <tr><td>Vid Quality:</td><td align="center" colspan="2"><input type="range" id="quality" min="10" max="63" value="10" onchange="try{fetch(document.location.origin+'/control?var=quality&val='+this.value);}catch(e){}"></td></tr> <tr><td>Vid Size:</td><td align="center" colspan="2"><input type="range" id="framesize" min="0" max="6" value="5" onchange="try{fetch(document.location.origin+'/control?var=framesize&val='+this.value);}catch(e){}"></td></tr> </table> </div> </section> </section> <script> document.addEventListener('DOMContentLoaded',function(){function b(B){let C;switch(B.type){case'checkbox':C=B.checked?1:0;break;case'range':case'select-one':C=B.value;break;case'button':case'submit':C='1';break;default:return;}const D=`${c}/control?var=${B.id}&val=${C}`;fetch(D).then(E=>{console.log(`request to ${D} finished, status: ${E.status}`)})}var c=document.location.origin;const e=B=>{B.classList.add('hidden')},f=B=>{B.classList.remove('hidden')},g=B=>{B.classList.add('disabled'),B.disabled=!0},h=B=>{B.classList.remove('disabled'),B.disabled=!1},i=(B,C,D)=>{D=!(null!=D)||D;let E;'checkbox'===B.type?(E=B.checked,C=!!C,B.checked=C):(E=B.value,B.value=C),D&&E!==C?b(B):!D&&('aec'===B.id?C?e(v):f(v):'agc'===B.id?C?(f(t),e(s)):(e(t),f(s)):'awb_gain'===B.id?C?f(x):e(x):'face_recognize'===B.id&&(C?h(n):g(n)))};document.querySelectorAll('.close').forEach(B=>{B.onclick=()=>{e(B.parentNode)}}),fetch(`${c}/status`).then(function(B){return B.json()}).then(function(B){document.querySelectorAll('.default-action').forEach(C=>{i(C,B[C.id],!1)})});const j=document.getElementById('stream'),k=document.getElementById('stream-container'),l=document.getElementById('get-still'),m=document.getElementById('toggle-stream'),n=document.getElementById('face_enroll'),o=document.getElementById('close-stream'),p=()=>{window.stop(),m.innerHTML='Start'},q=()=>{j.src=`${c+':81'}/stream`,f(k),m.innerHTML='Stop'};l.onclick=()=>{p(),j.src=`${c}/capture?_cb=${Date.now()}`,f(k)},o.onclick=()=>{p(),e(k)},m.onclick=()=>{const B='Stop'===m.innerHTML;B?p():q()},n.onclick=()=>{b(n)},document.querySelectorAll('.default-action').forEach(B=>{B.onchange=()=>b(B)});const r=document.getElementById('agc'),s=document.getElementById('agc_gain-group'),t=document.getElementById('gainceiling-group');r.onchange=()=>{b(r),r.checked?(f(t),e(s)):(e(t),f(s))};const u=document.getElementById('aec'),v=document.getElementById('aec_value-group');u.onchange=()=>{b(u),u.checked?e(v):f(v)};const w=document.getElementById('awb_gain'),x=document.getElementById('wb_mode-group');w.onchange=()=>{b(w),w.checked?f(x):e(x)};const y=document.getElementById('face_detect'),z=document.getElementById('face_recognize'),A=document.getElementById('framesize');A.onchange=()=>{b(A),5<A.value&&(i(y,!1),i(z,!1))},y.onchange=()=>{return 5<A.value?(alert('Please select CIF or lower resolution before enabling this feature!'),void i(y,!1)):void(b(y),!y.checked&&(g(n),i(z,!1)))},z.onchange=()=>{return 5<A.value?(alert('Please select CIF or lower resolution before enabling this feature!'),void i(z,!1)):void(b(z),z.checked?(h(n),i(y,!0)):g(n))}}); </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)); } void startCameraServer() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL }; httpd_uri_t status_uri = { .uri = "/status", .method = HTTP_GET, .handler = status_handler, .user_ctx = NULL }; httpd_uri_t cmd_uri = { .uri = "/control", .method = HTTP_GET, .handler = cmd_handler, .user_ctx = NULL }; httpd_uri_t capture_uri = { .uri = "/capture", .method = HTTP_GET, .handler = capture_handler, .user_ctx = NULL }; httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler, .user_ctx = NULL }; Serial.printf("Starting web server on port: '%d'\n", config.server_port); if (httpd_start(&camera_httpd, &config) == ESP_OK) { httpd_register_uri_handler(camera_httpd, &index_uri); httpd_register_uri_handler(camera_httpd, &cmd_uri); httpd_register_uri_handler(camera_httpd, &status_uri); httpd_register_uri_handler(camera_httpd, &capture_uri); } config.server_port += 1; config.ctrl_port += 1; Serial.printf("Starting stream server on port: '%d'\n", config.server_port); if (httpd_start(&stream_httpd, &config) == ESP_OK) { httpd_register_uri_handler(stream_httpd, &stream_uri); } } unsigned int get_speed(unsigned int sp) { return map(sp, 0, 100, 0, 255); } void robot_setup() { // Pins for Motor Controller pinMode(LEFT_M0,OUTPUT); pinMode(LEFT_M1,OUTPUT); pinMode(RIGHT_M0,OUTPUT); pinMode(RIGHT_M1,OUTPUT); // Make sure we are stopped robot_stop(); // Motor uses PWM Channel 8 ledcAttachPin(MTR_PWM, 8); ledcSetup(8, 2000, 8); ledcWrite(8, 130); } // Motor Control Functions void update_speed() { ledcWrite(motorPWMChannnel, get_speed(motor_speed)); } void robot_stop() { digitalWrite(LEFT_M0,LOW); digitalWrite(LEFT_M1,LOW); digitalWrite(RIGHT_M0,LOW); digitalWrite(RIGHT_M1,LOW); } void robot_fwd() { digitalWrite(LEFT_M0,HIGH); digitalWrite(LEFT_M1,LOW); digitalWrite(RIGHT_M0,HIGH); digitalWrite(RIGHT_M1,LOW); move_interval=250; previous_time = millis(); } void robot_back() { digitalWrite(LEFT_M0,LOW); digitalWrite(LEFT_M1,HIGH); digitalWrite(RIGHT_M0,LOW); digitalWrite(RIGHT_M1,HIGH); move_interval=250; previous_time = millis(); } void robot_right() { digitalWrite(LEFT_M0,LOW); digitalWrite(LEFT_M1,HIGH); digitalWrite(RIGHT_M0,HIGH); digitalWrite(RIGHT_M1,LOW); move_interval=100; previous_time = millis(); } void robot_left() { digitalWrite(LEFT_M0,HIGH); digitalWrite(LEFT_M1,LOW); digitalWrite(RIGHT_M0,LOW); digitalWrite(RIGHT_M1,HIGH); move_interval=100; previous_time = millis(); } |
Here are some of the sections of this file you might want to modify:
Lines 348 – 402 – HTML
This is the heart of the HTML that we are sending to our browser.
The buttons and sliders are arranged inside a couple of tables, which admittedly is not the best way to arrange them. Those of you proficient with CSS could probably create a much better layout, but this simple one gets the job done!
You can see that each button has a couple of distinguishing features:
- class – This relates to the class entry in the stylesheet, which determines what the button looks like. See further below for information about modifying the stylesheet.
- id – Each button requires a unique id name. T8his name is used in the return URL, to determine which button was pressed.
- onclick event – Each button has a JavaScript “onclick” event associated with it. This creates the return URL whenever the button is clicked. Look at the end of this event (ie. “control?var=flash&val=1”) to determine the string sent back when the button is clicked. This relates to “Decode Return URL Values” below.
You can add more buttons or change the function of the existing ones. The sliders work in a similar fashion.
Lines 488 – 542 – Robot Movement Functions
These are the functions that control the TB6612FNG motor controller module. By sending a pattern of HIGH and LOW signals to the four control lines (2 per motor) we can control the motor direction.
The move_interval variable sets the amount of time the robot will execute the command. You can adjust this value if you want the robot to take shorter or longer movements in response to a button press.
Lines 257 – 315 – Decode Return URL Values
This is a series of if-else statements. It examines the value extracted from the return URL string, and based upon that value it calls a function from the “robot movement functions” above.
If you add more buttons or sliders you will need to modify this section to include the return URL strings that they send back when used.
CSS File Modifications
In the app_httpd.cpp file you’ll find all of the CSS (cascading style sheets) on line 355. But it won’t be easy to edit the CSS here, as it’s minified.
Minification is the process of taking a file like a CSS or JavaScript file and stripping out all of the characters that are not used by your web browser. This includes space characters, carriage returns, and linefeeds. These characters make the file easier for humans to work with, but are just wasted bytes as far as your web browser is concerned. And, since each byte takes time to transmit and receive, removing these characters will improve performance.
In order to work with this code you’ll need to “unminify” the CSS. There are several websites that will allow you to do this, including this one.
Once you have edited the CSS you’ll probably want to minify it again, to save space and to (slightly) improve performance. A website like this one can do the job for you.
To make things easier I have also included a substitute version of the app_httpd.cpp file, in the ZIP download of the code (in the Resources section at the end of this article). It’s called “app_httpd_unminified_css.cpp” , and you’ll find it in the “unminified” folder.
The most likely modification you’ll want to make to this file is the code defining the look and feel of the buttons on the display.
Lines 469 through 488 have the style settings for the buttons. You can modify the existing ones or add new ones. Just make sure to change the “class” setting for your target button in order for the changes to take effect.
Robot Car Demo
Once you have your code ready you can upload it to the ESP32CAM through the FTDI adapter. If you are unsure as to how to do this, or if you haven’t set up your Arduino IDE to include the ESP32, you can see my articles on the ESP32 and the ESP32CAM for details.
After uploading it’s time to test out our robot car!
You’ll need to connect your controlling device (phone, tablet or computer) to the WiFi access point created by the ESP32CAM module. Here is how to go about doing that:
- Turn on the robot car.
- Wait until you see the ESP32CAM LED (the white one) flash, this indicates tha the WiFi access point has been established.
- Open the WiFi network settings on your controller device. Look for the ESP32-CAM Robot WiFi Network (if you renamed the network SSID in your code then you should, of course, look for that network instead).
- Connect to the network using the password “1234567890”. If you changed the password then, of course, you’ll need to use the new one you programmed.
- Once the network connection is established, open a web browser.
- In the Address Bar type the following IP address – 192.168.4.1
- You should now see the web-based interface.
The interface operation is illustrated below:
<INSERT REMOTE INTERFACE DIAGRAM>
Click the START button to start the video stream. If all is working you should see the output of the camera on the ESP32CAM.
Now try the motor controls to ensure that (a) they all function and (b) the car moves in the correct direction. If the direction is wrong then you likely have the motor polarity reversed, so fixing that is pretty simple.
You can also test the speed controls, video controls, and light switches to be sure that all is well.
And, assuming that everything checks out, you can now give your car a full test drive!
Conclusion
This project, in my opinion, has a lot going for it. It’s educational, it requires you to exercise skills like design and soldering, and when you’re done you’ll have an amusing and unique toy that you can proudly say you built yourself.
I’ve purposely left a few GPIO pins available, so you can experiment by adding more devices to the robot car. In my original design I had done this, I added a couple of servo motors to use as a pan and tilt for the camera, however, I removed them due to mounting considerations (and some strange behavior when the car was in a low-signal area). You could attempt to recreate that or add a few sensors instead.
How ever you choose to build this robot car I hope that you have as much fun with it as I did. And if you come up with a unique design why not share it with your friends on the DroneBot Workshop Forum? We’d all love to see what you come up with!
Parts List
Here are some components that you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.
COMING SOON!
Resources
Code for ESP32CAM Car – The code files you’ll need to make this car work, all in one ZIP file.
Getting Started with ESP32 – Learn to use the ESP32 with the Arduino IDE.
Getting Started with the ESP32CAM – Using the inexpensive ESP32CAM Module.
TB6612FNG H-Bridge – Learn how the TB6612FNG H-Bridge works, and how it is better than the L298N.


hello I was wondering what range you can get with this antenna because I want to make my own.
I love Dronebot Workshop; no one does a better job at producing clear yet detailed videos and articles with excellent production value. I think this video and its accompanying article are a prime example. This is a really nice, clean design based on readily available and inexpensive parts. Also, great minds think alike. I have also created an ESP32cam robot based on that same cheap robot chassis; https://github.com/Ezward/Esp32CameraRover2. It also hosts it’s own web application for control. It can be used as a first-person-view rover, but the focus is on autonomy. The robot uses the optical wheel encoders that come with the robot chassis. I’ve written a… Read more »
That looks like a bigger/better?? Antenna than the one I am using. could you give a source for that please.
Alan
Hello,
In which file can you change the IP address 192.168.4.1 to 192.168.6.1.
Thanks
Alan (age 72)
This page has info on the softAP configuration
https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/soft-access-point-class.html#softapip
Great article!
Have you considered to use OTA in stead of the USB connection to program the ESP. ElegantOTA can do this whit a few lines of code.
Wow, what a project! I really appreciate all the time, dedication and detail that went into designing, coding and documenting this.. thank you ..
Hi,
Is it possible to make the esp32-cam face detection values to show on a smart watch or a smartphone through the build in web interference? For example, I know that the esp32 has build in face recognition libraries that when you sync an IP address it displays a little screen with a live feed face recognition. is there a way to get rid of the screen and display just the face recognition values that it detects?
Your reply would be very helpful for my final year project in uni.
Kindest regards,
Max
I want to ask that can I use ESP32 Camera with BLE(Bluetooth Low Energy) for receiving the video stream ?. Thanks for reading !
I love it and I am building it.
Thank you
Hi, this is David
Please visit sposync.com.
I am trying robots video streaming and steering service.
For now, only drone – Pyparrot can be attached.
You can attach your robots if you have client-side code – python
(** it is in the instruction)
Hope this will enable more advanced robots service, in more intelligent way.
Thanks,
For a project of 4 wheel drive car i have to connect the extra 2 motors parallel with each one of these two that you already have?
you are so smote
Hi Bill, I have the project running fine but the camera is 90 deg. off – If I change the sensor settings s-set mirror to s,0 I get the image flipping 180 deg. but I can’t get it at normal setting.
Any suggestions.
Thanks
I had this problem too but you can fix it in the html/UI. It is particularly curious because a lot of the settings are not default here and the display is not configured as shown. You can change the rotation values in that code (the CPP tab), I also had to change the display flip tag from 1 to 0, (I’d expect 0 to be default but it wasn’t). Strange. The only problem now (that brought me back here) is that the Quality slider is reversed. I’m happy it works but good and bad is opposite what you’d expect also.… Read more »
This is the cause of the problem
<img id=”stream” scr=”” class=”rotate90″>
Hi Bill, I solved the camera 90 geg. off but I’m having problems with the buttons – when you press it it only stays on for a few seconds. I have the correct positions ( forward,reverse,left and wright) but the motors only get 1.4 to 2.5 volts because the button doesn’t stay on.
Any suggestions?
Thanks
Hi!
My name ist Robert too.
I just finished my robot. I have the 90° problem too. How did you fix that?
The buttons left and right are reversed, but forward and backward are correct!
I did not change the original code! What is wrong?
Great project. All I need is the controller and antenna.
Thanks again for your extreme efforts to explain what we do, and why we do it.
You are the best.
I get this error when compiling…
exit status 1
‘httpd_req_t’ was not declared in this scope
What the heck am I doing wrong?
I had the same issue.
You need to add the second block of code above as a new tab and name it “app_httpd.cpp” (The “.cpp” is where the issue lies.)
Maybe also use the download link for the code in the blue resources section near the bottom of the webpage to minimize copy and paste errors.
Dear Don Botting;
Did you get the solution of your error.Kindly, inform me because I’m suffering from the same error.
Best regards,
Jassim
you use a regulator to lower the voltage of the batteries to 5V .
I find that the esp32 comes with an integrates AMS1117-3.3 regulator that can handle power to 15V. So i wonder if an extra regulator has an advantage, .
Hello sir i have issue during compiling the app_httpd.cppCode…. In the line
#include “dl_lib_matrix3d.h”
The error is
dl_lib_matrix3d.h: No such file or directory
Please share a solution for this….
I had the same error with the arduino IDE, I switch to VS code and changed the name of the .ino file for MAIN.cpp and it works just fine. (server part).
Now I have to figure out how to make it works with my H-bridge motor controller Ln289.
I had the same problem when after I installed the latest version 2.0.2 from the esp32 board manager.
Only when I downgraded it to the 1.0.6 version this error was resolved.
HI, I have the same problem. How du i downgrade the version. I would be grateful for a step-by-step guide as i am a beginner in this field. THX
I just picked up a set of these esp32 cams. You have the best documentation I have found to demonstrate how to make it work. Thanks for posting. I’ll let you know how our little project goes once I figure out which way is up.
Thank you for all your work on this project. My goal is to creat a very small vehicle. I’m a 3D printer. I’ve been unable to link to yur code so I have been cutting and pasting and found out how to create folder for Arduino IDE. I don’t have a website but do summit to thingiverse as Gwiahir34. Again Thanks.
HI,
Sorry for my bad english !! thanks for your work.It’s very interessing.I learn full things. Ihave a problem, I can’t find the library esp_camera.h please can you help me? I don’t know how process .
Thank you very much if you can help me . JCD
Just getting started with the ESP32-CAM. Tring to learn as much as I can about it. this seems like a good project to work with. I have been doing Arduino for some time and the Dronebot videos have been helpful.
What is the difference between these WIFI Libraries? Who makes them?
#include “esp_wifi.h”
#include <WiFi.h>
What do these Libraries do?
#include “soc/soc.h”
#include “soc/rtc_cntl_reg.h”
Thanks
Hi, the code works well for me. However, is it possible to display a sensor value without reloading the webpage.
Just watched Robot Car. I was stumped on ESP32-CAM example code because I have never worked with HTTPD and was clueless about modifying for my need. I think your project will get me over the hump. Thank you!
He, nice that you makes it that easy to comment. Personely I apprciate very much your dry but efficient way to explain difficult subjects. No fuss. very good that I can read quietly the whole text. Thanks a lot Roland.
for your information: I’ a retired MD who had loved to study engeneering instead of medicine.
What module,/board do you suggest as the first of your codes doesn’t compile for the Wemos Wrover module. All of the others fail because of, apparently, there is no such file or directory! This is despite all the files/directories being downloaded and installed. Any clues would be helpful. I am using Arduino IDE version 1.8.13.
Hello, let me introduce myself, Douglas a retired electronic tech trained in the Army. I have spent about 20 years building industrial RF heating equipment (microwave to induction) and about the same in automotive exhaust mfg. Six axis welding robots and tube forming machines. Well I lke your YouTube streams. However I have not found this project very helpful. Yes I did use it for pinout / wiring guide and I have been trying to develop something useful, to me, from your code. About all I have accomplished is denting my head banging on the wall. Most of that is… Read more »
This is a very good project I have ever seen. I want to display the value of the temperature sensor on the vehicle’s control page, what should be adjusted in the section: “static const char PROGMEM INDEX_HTML[] = R”rawliteral(
<!doctype html> ….. “. Other members please help me.
Hi, I got this message when uploading the code;
Error compiling for board ESP32 Wrover Module.
Any idea how to get over this error?
Thanks
I appreciate all of the work that went in to this project but the orientation of the esp32 shown for the hardware does not work with the code. In order for this to work the esp32 needs to be rotated 90 degrees. Other programs like the one in the esp32 examples has the image alighned with the esp32 mounted vertically like the picture shown in the tutorial. I have tried a number of times to rotate the image in the css code but all failed. Normally this site is the best on the web and all works as advertised but… Read more »
Have you found a solution for the orientation?, I have the same problem
The solution I found is in the app_httpd.cpp line 355. The last CSS item is:.rotate90{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-o-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}
Change all of the rotate(270deg) to rotate(180deg)
It should work fine
Howdy – I have noticed that your keeping the “Video” in its “Vivo.com” Location and was wondering why you did not choose to “fill” the “background” with the Video and then “Layer” the Controls Over The Video – as if it were An Actual Working Driving App for a Smartphone.! ( Vivo .viv files were horrid and small yet “Single Page Only” video web pages that later merged with RealAudio and then into “Real Video” Files for Sharing Copy-Right-Free short movies and film clips. ) the Concept of a Full Video Background as a “Movie” is not new.the Concept of… Read more »
Hi, love this project. Code works a treat. I am not very experienced in HTML but have been modifying it to add some extra buttons for an underwater ROV that I’m putting together. Couple of questions:- 1. The HTML does not show on the web page and I can’t work out why not. For time being I have added a caption to the table of buttons. 2. I have been trying to work out how to get the video stream to default to ‘on’ instead of ‘off’. I can see the HTML for the button in the code but can’t… Read more »
Congratulations for the effort, you have some brilliant ideas.
I tried the code and it works correctly on the PC,
but on the phone the interface is more difficult to scale!
If you have your ESP32 Board Manager updated to 2.0.6, this program will fail, see : https://github.com/espressif/arduino-esp32/issues/7716
Hi,
I found this tutorial very helpful. However, I want to recreate this with my walking robot, which uses servo motors. I know that the ESP32-CAM does not have enough pins for my project, but I do have an ESP32-WROOM that I am using for the project, and I wonder if I would be able to upload the code to the ESP32-WROOM and attach the ESP32-CAM using the pins?
Thank you!
I have connected it but it is giving me errors such as
[E][camera.c:1049] camera_probe(): Detected camera not supported.
[E][camera.c:1249] esp_camera_init(): Camera probe failed with error 0x20004
How do I fix this?
does not compile in Arduino IDE 1.8 – “dl_lib_matrix3d.h: No such file or directory”
Oh yeah, if your board is updated past 1.0.6, it will not work. You could fix this by going to version 1.0.6 or older in the board manager or removing everything that has to do with dl_lib_matrix3d.h
Hello, Can it be posssible to build an robot car which will perform object detection by the Esp32 cam module & movement with the help of the Arduino board ?.
I’ve already watched your object detection tutorial & it worked perfectly.
Good day, I’m still rather new to all of this and could use some insight, if I haven’t included necessary information or did this wrong please inform me and I will correct myself ASAP. Thank you in advance. I downloaded the zip to use as a starter point. When I try to compile I get a errors. I just opened the file and tried to compile without any mods. I’m not sure if I’m supposed to post this here or not but this is my setup Arduino: 1.8.19 (Windows Store 1.8.57.0) (Windows 10), Board: “ESP32 Wrover Module, Huge APP (3MB… Read more »
I solved my issue by installing Arduino ide esp32 core 1.0.6.
I’ve encountered a issue that thus far is leaving me confused lol. I’m trying to figure out how to modify this code to replace the TB6612FNG with a DRV8833. I know I can just buy the TB6612FNG but I am trying to learn not just copy and paste. Is anyone aware of a good resource as what I’ve found thus far seems conflicting or outright wrong. Tyia.
Hello. Please tell me. I made the car as shown in yours. The control works, but the camera sends one photo after connecting. the image seems to consist of two halves. After about 5 seconds the image disappears completely. the image appears only after the ESP is rebooted.
A big fan of your work and thanks for the shared knowledge. please i need help with the code when i complied it, i got an error message” ladcsetup was not declared do you mean led_stop”
am stuck here, i need help please.
Hey, I faced error compiling as the dl_lib_matrix3d.h was showing not declared? Do I need to download this library? How do I fix this?
Thanks