๐ต️♂️ 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.
• 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
- XIAO ESP32-S3 Sense – Buy Here
- N20 Motors (4x) – Buy Here
- TP4056 Charging Module – Buy Here
- 3.7V LiPo Battery 1000mAh – Buy Here
- 4mm Slide Switch – Buy Here
- Jumper Wires – Buy Here
- DRV8833 Motor Driver (2x) – Buy Here
- MT3608 Booster Module – Buy Here
- 8mm White LEDs (4x) – Buy Here
- 220 Ohm Resistors (4x) – Buy Here
๐จ️ 3D Print Files + Materials
๐ Download STL files (chassis, wheels, top cover, camera mount):
Recommended filaments:
๐งฑ Base + Top → 3201PA - F Nylone
๐ Wheels → 1172Pro Nylone
⚫ Tyres → TPU
๐ง Print with 0.2mm layer, 40% infill.
๐ Pin Connections (XIAO ESP32-S3 Sense)
| Component | XIAO Pin | Notes |
|---|---|---|
| DRV8833 #1 (Left motors) | D0, D1, D2, D3 | IN1/IN2 – Motor A ; IN3/IN4 – Motor B |
| DRV8833 #2 (Right motors) | D4, D5, D6, D7 | corresponding control pins |
| LED Bar (4x white) via 220ฮฉ | D8 | Active HIGH (tactical light) |
| MT3608 Boost (5V out) | 5V / GND | powers ESP32 & camera |
| TP4056 + Battery | OUT+ → 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
Post a Comment