Skip to main content

DIY SPY CAR

DIY SPY CAR | XIAO ESP32-S3 Sense | 4WD FPV Recon

๐Ÿ•ต️‍♂️ DIY SPY CAR · XIAO ESP32-S3 SENSE

⚡ 4WD | FPV Live-Stream | Tactical UI | Night Ops LED | WebSocket Control

๐Ÿ”Ž What is SPY CAR?

A compact 4WD reconnaissance robot built around the XIAO ESP32-S3 Sense. Streams live video to any smartphone, features tactical white LEDs, 4 individually controlled N20 motors, real‑time CPU temperature and a military‑style web dashboard. Perfect for FPV exploration, spy‑themed projects or RC fun.

⚙️ How It Works

• XIAO ESP32-S3 Sense captures QVGA video (JPEG) → streamed via HTTP.
• Two DRV8833 dual H‑bridge drivers control 4 N20 motors (tank steering).
• MT3608 boost converter lifts 3.7V LiPo → stable 5V for ESP32 and camera.
• WebSocket server handles real‑time commands (forward/back/pivot/stop) with ultra-low latency.
• Tactical LED bar (4x white LEDs) activated from dashboard.
• Integrated temperature sensor sends core readings to web UI.

๐Ÿ“ฆ Components Required

๐Ÿ–จ️ 3D Print Files + Materials

๐Ÿ”Œ Pin Connections (XIAO ESP32-S3 Sense)

ComponentXIAO PinNotes
DRV8833 #1 (Left motors)D0, D1, D2, D3IN1/IN2 – Motor A ; IN3/IN4 – Motor B
DRV8833 #2 (Right motors)D4, D5, D6, D7corresponding control pins
LED Bar (4x white) via 220ฮฉD8Active HIGH (tactical light)
MT3608 Boost (5V out)5V / GNDpowers ESP32 & camera
TP4056 + BatteryOUT+ → MT3608 IN+3.7V LiPo → boost converter
⚡ Important: Adjust MT3608 to 5.0V before connecting to XIAO. Use a slide switch between battery + and TP4056 input for safety.

⚡ Features & Performance

๐Ÿ“น FPV Live Video (QVGA 320x240) / low latency
๐ŸŽฎ WebSocket commands → instant movement
๐ŸŒก️ Real‑time Core temperature + uptime
๐Ÿ’ก Tactical LED Bar (night mode)
๐Ÿ”‹ 1000mAh LiPo → 45+ min runtime
⚙️ Tank steering & 4WD traction

๐Ÿ’ป Arduino Firmware (Full Code)

/*
  By SIMPLE CIRCUITS
  SPY CAR - XIAO ESP32-S3 Sense with DRV8833 Dual Motor Drivers
  - 4 N20 motors individually controlled
  - Military Themed Web Interface
  - LED control on D8
  - Internal temperature monitoring
*/

#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <esp_camera.h>
#include <Arduino.h>

// ==================== WiFi Credentials ====================
const char* ssid = "SPY_CAR_HOTSPOT";
const char* password = "spycar1234";

// ==================== Pin Definitions ====================
#define LEFT_IN1    D0
#define LEFT_IN2    D1
#define LEFT_IN3    D2
#define LEFT_IN4    D3

#define RIGHT_IN1   D4
#define RIGHT_IN2   D5
#define RIGHT_IN3   D6
#define RIGHT_IN4   D7

#define LED_PIN     D8

// ==================== Camera Pin Definitions ====================
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      10
#define SIOD_GPIO_NUM      40
#define SIOC_GPIO_NUM      39
#define Y9_GPIO_NUM        48
#define Y8_GPIO_NUM        11
#define Y7_GPIO_NUM        12
#define Y6_GPIO_NUM        14
#define Y5_GPIO_NUM        16
#define Y4_GPIO_NUM        18
#define Y3_GPIO_NUM        17
#define Y2_GPIO_NUM        15
#define VSYNC_GPIO_NUM     38
#define HREF_GPIO_NUM      47
#define PCLK_GPIO_NUM      13

// ==================== Global Variables ====================
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
bool ledState = false;
float cpuTemp = 0;

// ==================== Motor Control Functions ====================
#define MOTOR_FORWARD   HIGH
#define MOTOR_BACKWARD  LOW

void setMotorPins(int in1, int in2, bool forward) {
  if (forward) {
    digitalWrite(in1, MOTOR_FORWARD);
    digitalWrite(in2, MOTOR_BACKWARD);
  } else {
    digitalWrite(in1, MOTOR_BACKWARD);
    digitalWrite(in2, MOTOR_FORWARD);
  }
}

void stopMotorPins(int in1, int in2) {
  digitalWrite(in1, LOW);
  digitalWrite(in2, LOW);
}

void moveForward() {
  setMotorPins(LEFT_IN1, LEFT_IN2, true);
  setMotorPins(LEFT_IN3, LEFT_IN4, true);
  setMotorPins(RIGHT_IN1, RIGHT_IN2, true);
  setMotorPins(RIGHT_IN3, RIGHT_IN4, true);
}

void moveBackward() {
  setMotorPins(LEFT_IN1, LEFT_IN2, false);
  setMotorPins(LEFT_IN3, LEFT_IN4, false);
  setMotorPins(RIGHT_IN1, RIGHT_IN2, false);
  setMotorPins(RIGHT_IN3, RIGHT_IN4, false);
}

void rotateLeft() {
  setMotorPins(LEFT_IN1, LEFT_IN2, false);
  setMotorPins(LEFT_IN3, LEFT_IN4, false);
  setMotorPins(RIGHT_IN1, RIGHT_IN2, true);
  setMotorPins(RIGHT_IN3, RIGHT_IN4, true);
}

void rotateRight() {
  setMotorPins(LEFT_IN1, LEFT_IN2, true);
  setMotorPins(LEFT_IN3, LEFT_IN4, true);
  setMotorPins(RIGHT_IN1, RIGHT_IN2, false);
  setMotorPins(RIGHT_IN3, RIGHT_IN4, false);
}

void stopAllMotors() {
  stopMotorPins(LEFT_IN1, LEFT_IN2);
  stopMotorPins(LEFT_IN3, LEFT_IN4);
  stopMotorPins(RIGHT_IN1, RIGHT_IN2);
  stopMotorPins(RIGHT_IN3, RIGHT_IN4);
}

void toggleLED() {
  ledState = !ledState;
  digitalWrite(LED_PIN, ledState ? HIGH : LOW);
}

void readTemperature() {
  cpuTemp = temperatureRead();
}

// ==================== Camera Initialization ====================
void initCamera() {
  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.frame_size = FRAMESIZE_QVGA;
  config.pixel_format = PIXFORMAT_JPEG;
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x\n", err);
    return;
  }
  
  sensor_t * s = esp_camera_sensor_get();
  if (s) {
    s->set_vflip(s, 1);
    s->set_hmirror(s, 1);
  }
}

// ==================== Web Interface HTML ====================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>SPY CAR CONTROLLER</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            background: #0d1117;
            font-family: monospace;
            padding: 20px;
        }
        .container {
            max-width: 500px;
            margin: auto;
            background: #161b22;
            border-radius: 16px;
            padding: 20px;
        }
        button {
            background: #238636;
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 5px;
            border-radius: 8px;
            cursor: pointer;
        }
        .cam {
            background: black;
            text-align: center;
            padding: 10px;
            margin-bottom: 10px;
        }
        img { width: 100%; }
        .info { margin-top: 10px; font-size: 12px; }
    </style>
</head>
<body>
<div class="container">
    <h3>SPY CAR CONTROL</h3>
    <div class="cam"><img id="camera-feed" src="/stream"></div>
    <button id="ledBtn">๐Ÿ’ก LED TOGGLE</button>
    <div>
        <button id="fwd">▲ FORWARD</button>
        <button id="rev">▼ BACKWARD</button>
        <button id="left">◀ LEFT</button>
        <button id="right">▶ RIGHT</button>
        <button id="stop">⏹ STOP</button>
    </div>
    <div class="info">Temp: <span id="temp">--</span>°C | Uptime: <span id="uptime">0</span>s</div>
</div>
<script>
let ws;
function connect() {
    ws = new WebSocket(`ws://${location.hostname}:81`);
    ws.onmessage = e => { if(e.data.startsWith('temp:')) document.getElementById('temp').innerText = parseFloat(e.data.split(':')[1]).toFixed(1); };
}
function send(c) { if(ws && ws.readyState === WebSocket.OPEN) ws.send(c+':1'); }
document.getElementById('fwd').onmousedown = () => send('forward');
document.getElementById('fwd').onmouseup = () => send('stop');
document.getElementById('rev').onmousedown = () => send('backward');
document.getElementById('rev').onmouseup = () => send('stop');
document.getElementById('left').onmousedown = () => send('left');
document.getElementById('left').onmouseup = () => send('stop');
document.getElementById('right').onmousedown = () => send('right');
document.getElementById('right').onmouseup = () => send('stop');
document.getElementById('stop').onclick = () => send('stop');
let led = false;
document.getElementById('ledBtn').onclick = () => { led = !led; send('led:' + (led?1:0)); };
setInterval(() => { let img = document.getElementById('camera-feed'); img.src = '/stream?_=' + Date.now(); }, 100);
let start = Date.now();
setInterval(() => { document.getElementById('uptime').innerText = Math.floor((Date.now()-start)/1000); }, 1000);
connect();
</script>
</body>
</html>
)rawliteral";

// ==================== WebSocket Event Handler ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
  if (type == WStype_TEXT) {
    String command = String((char*)payload);
    int sep = command.indexOf(':');
    if (sep > 0) {
      String cmd = command.substring(0, sep);
      String val = command.substring(sep + 1);
      
      if (cmd == "forward") {
        rotateRight();
      } 
      else if (cmd == "backward") {
        rotateLeft();
      }
      else if (cmd == "left") {
        moveBackward();
      }
      else if (cmd == "right") {
        moveForward();
      }
      else if (cmd == "stop") {
        stopAllMotors();
      }
      else if (cmd == "led") {
        if (val == "1" && !ledState) toggleLED();
        else if (val == "0" && ledState) toggleLED();
      }
    }
  }
}

// ==================== HTTP Request Handlers ====================
void handleRoot() {
  server.send(200, "text/html", index_html);
}

void handleStream() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "Camera capture failed");
    return;
  }
  server.sendHeader("Content-Type", "image/jpeg");
  server.sendHeader("Content-Length", String(fb->len));
  server.send_P(200, "image/jpeg", (const char*)fb->buf, fb->len);
  esp_camera_fb_return(fb);
}

void handleNotFound() {
  server.send(404, "text/plain", "Not Found");
}

// ==================== Setup ====================
void setup() {
  Serial.begin(115200);
  Serial.println("\n=== SPY CAR CONTROLLER ===");
  
  pinMode(LEFT_IN1, OUTPUT);
  pinMode(LEFT_IN2, OUTPUT);
  pinMode(LEFT_IN3, OUTPUT);
  pinMode(LEFT_IN4, OUTPUT);
  pinMode(RIGHT_IN1, OUTPUT);
  pinMode(RIGHT_IN2, OUTPUT);
  pinMode(RIGHT_IN3, OUTPUT);
  pinMode(RIGHT_IN4, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  
  stopAllMotors();
  digitalWrite(LED_PIN, LOW);
  
  initCamera();
  
  Serial.println("Connecting to WiFi...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    attempts++;
  }
  
  IPAddress myIP;
  
  if (WiFi.status() == WL_CONNECTED) {
    myIP = WiFi.localIP();
    Serial.println("WiFi Connected!");
  } else {
    WiFi.mode(WIFI_AP);
    WiFi.softAP(ssid, password);
    myIP = WiFi.softAPIP();
    Serial.println("AP Mode Active");
  }
  
  server.on("/", handleRoot);
  server.on("/stream", handleStream);
  server.onNotFound(handleNotFound);
  server.begin();
  
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  
  Serial.print("IP: ");
  Serial.println(myIP);
  Serial.println("SPY CAR CONTROLLER READY");
}

void loop() {
  server.handleClient();
  webSocket.loop();
  
  static unsigned long lastTempUpdate = 0;
  if (millis() - lastTempUpdate > 2000) {
    readTemperature();
    String tempMsg = "temp:" + String(cpuTemp, 1);
    webSocket.broadcastTXT(tempMsg);
    lastTempUpdate = millis();
  }
  
  delay(1);
}
๐Ÿ“ก Code highlights: creates WiFi AP "SPY_CAR_HOTSPOT" (pw: spycar1234), serves real‑time dashboard, WebSocket low‑latency driving, camera stream at QVGA, automatic temperature broadcast.

๐Ÿ› ️ Assembly (6 steps)

  • 1️⃣ 3D print all parts (Nylon base + TPU tyres).
  • 2️⃣ Solder DRV8833 modules, connect motors to outputs and XIAO pins D0-D7.
  • 3️⃣ Set MT3608 to 5.0V, connect to 5V/GND of XIAO.
  • 4️⃣ Wire slide switch, TP4056, battery and boost converter.
  • 5️⃣ LED bar: 4x white LEDs + 220ฮฉ resistors to D8 and GND.
  • 6️⃣ Upload sketch, power ON, connect phone to SPY_CAR_HOTSPOT. Open browser → 192.168.4.1

๐ŸŽ“ Design your own PCBs with Altium

Altium makes custom PCB design fast and professional. Students get free access to Altium Student Lab – step‑by‑step courses & certifications.

๐Ÿ‘‰ Explore Altium Student Lab →
๐Ÿ” Tip: For better night vision, add IR LEDs (940nm) parallel to white LEDs. Adjust camera quality (jpeg_quality) for smoother FPS.

Comments

Popular posts from this blog

Solar Tracking System

Dual-Axis Solar Tracking System | Arduino SimpleCircuits Arduino Solar DIY Renewable Energy Project Dual-Axis Solar Tracking System Build a high-performance solar tracker that follows the sun in real-time. Four LDR sensors, an Arduino UNO, and two servo motors — boosting energy capture by up to 40% versus fixed mounts. 40% More Efficient 4 LDR Sensors 2 Servo Axes <300mA Power Draw Finished Build System Overview How the System Works The tracker reads the sky through four LDR sensors placed around the panel, computes intensity deltas, and drives two SG90 servos in a real-time closed loop — keeping the panel perpendicular to sunlight from sunrise to sunset. ...

Arduino Code Car Parking System

 // Created by Simple Circuits  #include <Wire.h>  #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27,16,2);    #include <Servo.h>  Servo myservo; int IR1 = 2; int IR2 = 3; int Slot = 4;           //Total number of parking Slots int flag1 = 0; int flag2 = 0; void setup() {   Serial.begin(9600);      lcd.init(); //initialize the lcd     lcd.backlight(); //open the backlight     pinMode(IR1, INPUT); pinMode(IR2, INPUT);    myservo.attach(4); myservo.write(100); lcd.setCursor (0,0); lcd.print("     ARDUINO    "); lcd.setCursor (0,1); lcd.print(" PARKING SYSTEM "); delay (2000); lcd.clear();   } void loop(){  if(digitalRead (IR1) == LOW && flag1==0){ if(Slot>0){flag1=1; if(flag2==0){myservo.write(0); Slot = Slot-1;} }else{ lcd.setCursor (0,0); lcd.print("    SORRY :(    ");   lc...

Arduino Code

 //define Pins #include <Servo.h> Servo servo; int trigPin = 11; int echoPin = 12; // defines variables long duration; int distance; void setup()  {   servo.attach(13);   servo.write(180);  delay(2000);    // Sets the trigPin as an Output pinMode(trigPin, OUTPUT); // Sets the echoPin as an Input  pinMode(echoPin, INPUT); } void loop()  { // Clears the trigPin digitalWrite(trigPin, LOW); delayMicroseconds(2); // Sets the trigPin on HIGH state for 10 micro seconds digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); // Reads the echoPin, returns the sound wave travel time in microseconds duration = pulseIn(echoPin, HIGH); // Calculating the distance distance= duration*0.034/2; // Prints the distance on the Serial Monitor Serial.print("Distance: "); Serial.println(distance); if ( distance <= 25   ) // Change Distance according to Ultrasonic Sensor Placement  { servo.write(180); delay(3000); ...