Romeo ESP32-S3 Robot Development Board With The Devastator Tank Mobile Robot Platform

userHead John.Winters 2026-04-01 13:09:07 310 Views4 Replies

I am trying to make a web based FPV tank with the Devastator Tank Mobile Robot Platform(R0B0112) and Romeo ESP32-S3 Robot Development Board(DFR0994).

 

My code works when not streaming video  (I comment out line 155).  But when I try to stream the control buttons do not work, until I close my browser then the tank tries to runaway.  Any ideas?  

 

— Code  ----

#include <HTTP_Method.h>

#include <Middlewares.h>

#include <Uri.h>

#include "esp_camera.h"

#include <WiFi.h>

#include <WebServer.h>

#include "DFRobot_AXP313A.h"


 

// --- Camera Pinout for Romeo S3 (standard OV2640) ---

#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3

#include "camera_pins.h"


 

DFRobot_AXP313A axp;


 

const char* ssid = "Devastator_FPV";

const char* password = "password123";


 

WebServer server(80);

WiFiServer streamServer(81);


 

// Motor Pins

const int M1_PWM = 12; // Adjust based on your Romeo S3 version

const int M1_DIR = 13;

const int M2_PWM = 14;

const int M2_DIR = 21;


 

void setup() {

Serial.begin(115200);


 

while (axp.begin() !=0){

Serial.println("init error");

delay(1000);

}

axp.enableCameraPower(axp.eOV2640);


 

// 1. Camera Config


 

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.frame_size = FRAMESIZE_UXGA;

config.pixel_format = PIXFORMAT_JPEG; // for streaming

//config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition

config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;

config.fb_location = CAMERA_FB_IN_PSRAM;

config.jpeg_quality = 32;

config.fb_count = 1;


 

// if PSRAM IC present, init with UXGA resolution and higher JPEG quality

// for larger pre-allocated frame buffer.

if (config.pixel_format == PIXFORMAT_JPEG) {

if (psramFound()) {

config.jpeg_quality = 10;

config.fb_count = 2;

config.grab_mode = CAMERA_GRAB_LATEST;

} else {

// Limit the frame size when PSRAM is not available

config.frame_size = FRAMESIZE_SVGA;

config.fb_location = CAMERA_FB_IN_DRAM;

}

} else {

// Best option for face detection/recognition

config.frame_size = FRAMESIZE_240X240;

#if CONFIG_IDF_TARGET_ESP32S3

config.fb_count = 2;

#endif

}


 

#if defined(CAMERA_MODEL_ESP_EYE)

pinMode(13, INPUT_PULLUP);

pinMode(14, INPUT_PULLUP);

#endif


 

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

}


 

sensor_t *s = esp_camera_sensor_get();

// initial sensors are flipped vertically and colors are a bit saturated

if (s->id.PID == OV3660_PID) {

s->set_vflip(s, 1); // flip it back

s->set_brightness(s, 1); // up the brightness just a bit

s->set_saturation(s, -2); // lower the saturation

}

// drop down frame size for higher initial frame rate

if (config.pixel_format == PIXFORMAT_JPEG) {

s->set_framesize(s, FRAMESIZE_QVGA);

}



 

// 2. WiFi & Motors

pinMode(M1_PWM, OUTPUT); pinMode(M1_DIR, OUTPUT);

pinMode(M2_PWM, OUTPUT); pinMode(M2_DIR, OUTPUT);

 

WiFi.softAP(ssid, password);

Serial.println("AP Started. IP: 192.168.4.1");

 

// 3. Routes

server.on("/", handleRoot);

server.on("/action", handleAction);

server.begin();

streamServer.begin();

}


 

void loop() {

server.handleClient();

handleStream();

}


 

// --- Video Streaming Logic ---

void handleStream() {

WiFiClient client = streamServer.available();

if (client) {

client.println("HTTP/1.1 200 OK");

client.println("Content-Type: multipart/x-mixed-replace; boundary=frame");

client.println();


 

while (client.connected()) {

camera_fb_t * fb = esp_camera_fb_get();

if (!fb) break;


 

client.printf("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", fb->len);

client.write(fb->buf, fb->len);

client.print("\r\n");

esp_camera_fb_return(fb);

}

}

}


 

// --- Web UI ---

void handleRoot() {

String html = "<html><body style='text-align:center; background:#222; color:white; font-family:sans-serif;'>";

html += "<h1>Devastator FPV</h1>";

// The Image source points to the stream on port 81

//html += "<img src='http://192.168.4.1:81' style='width:320px; border:2px solid orange;'><br><br>";

html += "<H2>";

html += "<button style='width:100px;height:50px' onmousedown=\"fetch('/action?dir=F')\" onmouseup=\"fetch('/action?dir=S')\">FORWARD</button><br>";

html += "<button style='width:100px;height:50px' onmousedown=\"fetch('/action?dir=L')\" onmouseup=\"fetch('/action?dir=S')\">LEFT</button>";

html += "<button style='width:100px;height:50px' onmousedown=\"fetch('/action?dir=R')\" onmouseup=\"fetch('/action?dir=S')\">RIGHT</button><br>";

html += "<button style='width:100px;height:50px' onmousedown=\"fetch('/action?dir=B')\" onmouseup=\"fetch('/action?dir=S')\">BACK</button>";

html += "</H2>";

server.send(200, "text/html", html);

}


 

void handleAction() {

String dir = server.arg("dir");

if (dir == "F") move(150, 150, 1, 0);

else if (dir == "B") move(150, 150, 0, 1);

else if (dir == "L") move(200, 200, 0, 0);

else if (dir == "R") move(200, 200, 1, 1);

else move(0, 0, 0, 0);

server.send(200);

}


 

void move(int sL, int sR, bool dL, bool dR) {

digitalWrite(M1_DIR, dL); digitalWrite(M2_DIR, dR);

analogWrite(M1_PWM, sL); analogWrite(M2_PWM, sR);

}


  

2026-04-02 02:47:31

That got it!  Thanks again.  I just asked gemini to add delays so that the code would work and it eventually got it.  This worked

 

#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#include "DFRobot_AXP313A.h"

// --- Camera Pinout for Romeo S3 ---
#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 
#include "camera_pins.h"

DFRobot_AXP313A axp;
const char* ssid = "Devastator_FPV";
const char* password = "password123";

WebServer server(80);
WiFiServer streamServer(81);

// Motor Pins
const int M1_PWM = 12; 
const int M1_DIR = 13;
const int M2_PWM = 14;
const int M2_DIR = 21;

void setup() {
 Serial.begin(115200);

 // 1. Power Management - Critical for Romeo S3
 if (axp.begin() != 0) {
   Serial.println("AXP313A init error");
 }  
 axp.enableCameraPower(axp.eOV2640);

 // 2. Camera Configuration
 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.frame_size = FRAMESIZE_QVGA;
 config.pixel_format = PIXFORMAT_JPEG;
 config.grab_mode = CAMERA_GRAB_LATEST;
 config.fb_location = CAMERA_FB_IN_PSRAM;
 config.jpeg_quality = 12;
 config.fb_count = 2;

 esp_err_t err = esp_camera_init(&config);
 if (err != ESP_OK) {
   Serial.printf("Camera init failed: 0x%x", err);
 }

 // 3. Motor Pin Setup
 pinMode(M1_PWM, OUTPUT); 
 pinMode(M1_DIR, OUTPUT);
 pinMode(M2_PWM, OUTPUT); 
 pinMode(M2_DIR, OUTPUT);
 
 WiFi.softAP(ssid, password);
 
 // 4. Routes
 server.on("/", handleRoot);
 server.on("/action", handleAction);
 server.begin();
 streamServer.begin();

 Serial.println("System Ready. Connect to Devastator_FPV");
}

void loop() {
 // Handle web server requests (Motor Commands)
 server.handleClient();
 
 // Handle streaming (Video)
 handleStream();

 // CRITICAL: Small delay to prevent watchdog triggers 
 // and allow the background WiFi task to process actions.
 delay(1); 
}

void handleAction() {
 String dir = server.arg("dir");
 Serial.print("Command Received: "); Serial.println(dir);

 if (dir == "F")      move(180, 180, 1, 0);
 else if (dir == "B") move(180, 180, 0, 1);
 else if (dir == "L") move(200, 200, 0, 0);
 else if (dir == "R") move(200, 200, 1, 1);
 else                 move(0, 0, 0, 0); // Stop

 server.send(200, "text/plain", "OK");
}

void move(int sL, int sR, bool dL, bool dR) {
 digitalWrite(M1_DIR, dL); 
 digitalWrite(M2_DIR, dR);
 analogWrite(M1_PWM, sL); 
 analogWrite(M2_PWM, sR);
}

void handleStream() {
 static WiFiClient client;
 
 // If no client is connected, check for a new one
 if (!client || !client.connected()) {
   client = streamServer.available();
   if (client) {
     client.println("HTTP/1.1 200 OK");
     client.println("Content-Type: multipart/x-mixed-replace; boundary=frame");
     client.println();
   }
 }

 // If a client IS connected, send ONE frame then exit to allow loop() to continue
 if (client && client.connected()) {
   camera_fb_t * fb = esp_camera_fb_get();
   if (fb) {
     client.printf("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", fb->len);
     client.write(fb->buf, fb->len);
     client.print("\r\n");
     esp_camera_fb_return(fb);
   }
 }
}

void handleRoot() {
 String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'></head>";
 html += "<body style='text-align:center; background:#222; color:white;'>";
 html += "<h2>Romeo S3 Control</h2>";
 html += "<img src='http://192.168.4.1:81' style='width:320px;'><br><br>";
 // Script-based fetch is faster than standard links
 html += "<button style='width:80px;height:80px' onmousedown=\"fetch('/action?dir=F')\" onmouseup=\"fetch('/action?dir=S')\" ontouchstart=\"fetch('/action?dir=F')\" ontouchend=\"fetch('/action?dir=S')\">FWD</button><br>";
 html += "<button style='width:80px;height:80px' onmousedown=\"fetch('/action?dir=L')\" onmouseup=\"fetch('/action?dir=S')\" ontouchstart=\"fetch('/action?dir=L')\" ontouchend=\"fetch('/action?dir=S')\">L</button>";
 html += "<button style='width:80px;height:80px' onmousedown=\"fetch('/action?dir=R')\" onmouseup=\"fetch('/action?dir=S')\" ontouchstart=\"fetch('/action?dir=R')\" ontouchend=\"fetch('/action?dir=S')\">R</button><br>";
 html += "<button style='width:80px;height:80px' onmousedown=\"fetch('/action?dir=B')\" onmouseup=\"fetch('/action?dir=S')\" ontouchstart=\"fetch('/action?dir=B')\" ontouchend=\"fetch('/action?dir=S')\">REV</button>";
 html += "</body></html>";
 server.send(200, "text/html", html);
}

userHeadPic John.Winters
2026-04-01 22:47:32

One possible problem could be: CPU Blocking. The primary reason the tank's controls fail when video starts is the while(client.connected()) loop inside the handleStream() function.

 

The Trap: Once a browser connects to the video stream (Port 81), the code enters that while loop and stays there indefinitely to keep sending JPEG frames.

 

The Consequence: Because the CPU is stuck in that loop, it never returns to loop() to run server.handleClient(). This is why the control buttons (Port 80) stop working.

 

The "Runaway" Effect: When the browser is closed, the connection breaks and the loop finally ends. However, if the last command received was "Forward," the motor pins stay HIGH because the "Stop" command (usually sent on mouseup) was never processed.

 

 

userHeadPic R2D2C3PO
John.Winters wrote:

Thank you so much for your answer.  I think you are right.  

I will look into fixing this.  

 

 

My other thought was the battery did not have enough amps,  I am using a 4S battery 850 mah.  I got a splitter and UBEC to use it to power the motors.  

https://www.youtube.com/shorts/k8GCJCmK-U0

2026-04-02 00:37:13
1 Replies