Home › Projects › Phone-Controlled ESP32 Car

Phone-Controlled ESP32 Car: drive it from any browser, no app needed

Beginner~40 minutesSame hardware as the obstacle carESP32 core 3.x

The ESP32 has Wi-Fi built in, so it can be its own hotspot and web server. Join its network from your phone, open one address, and you get arrow buttons and a speed slider.

✓ Compile-checked on 2026-09-29 with the Arduino ESP32 board package 3.3.12 (all warnings enabled, 0 warnings), board: ESP32 Dev Module.

1. What you need

The same car, L298N and wiring as the obstacle-avoiding car. The ultrasonic sensor can stay or go.

2. How it works

  1. Access point mode: WiFi.softAP() creates a network called TinyBot-Car. Its address is always 192.168.4.1.
  2. Web server: WebServer serves the controller page, which lives inside the sketch — no internet needed.
  3. Commands: while you hold a button, the phone sends /cmd?c=F (etc.) every 200 ms; releasing sends stop.
  4. Failsafe: no command for 0.6 s and the car stops. If your phone drops the connection, the car won't run away.

3. Code

Change AP_PASS to your own password (at least 8 characters) before uploading.

/*
 * TinyBot Forge — Phone-Controlled ESP32 Robot
 * https://tinybotforge.com/en/
 *
 * The ESP32 creates its own Wi-Fi hotspot. Join it with your phone and open
 * http://192.168.4.1 to drive. No router, no app.
 * Hardware and wiring are the same as the ESP32 obstacle car (L298N + two TT motors).
 * Toolchain: Arduino IDE 2.x + ESP32 board package 3.x
 */
#include <WiFi.h>
#include <WebServer.h>

// ---------- Hotspot settings (password: at least 8 characters, change it!) ----------
const char *AP_SSID = "TinyBot-Car";
const char *AP_PASS = "tinybot123";

// ---------- Motor pins (same as the obstacle car) ----------
const int PIN_ENA = 25, PIN_IN1 = 26, PIN_IN2 = 27;
const int PIN_ENB = 33, PIN_IN3 = 32, PIN_IN4 = 13;
const int PWM_FREQ = 1000, PWM_BITS = 8;
const unsigned long FAILSAFE_MS = 600;  // stop if no command arrives for this long

WebServer server(80);
int speedValue = 180;
unsigned long lastCommandMs = 0;

void setMotor(int pinEn, int pinA, int pinB, int speed) {
  speed = constrain(speed, -255, 255);
  digitalWrite(pinA, speed > 0 ? HIGH : LOW);
  digitalWrite(pinB, speed < 0 ? HIGH : LOW);
  ledcWrite(pinEn, abs(speed));
}

void drive(int left, int right) {
  setMotor(PIN_ENA, PIN_IN1, PIN_IN2, left);
  setMotor(PIN_ENB, PIN_IN3, PIN_IN4, right);
}

// Controller page: sends a command every 200 ms while a button is held, and stop on release
const char PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>TinyBot Remote</title>
<style>
body{font-family:sans-serif;background:#1b1d1f;color:#eee;text-align:center;margin:0;padding:20px;user-select:none;-webkit-user-select:none}
.pad{display:grid;grid-template-columns:repeat(3,90px);gap:12px;justify-content:center;margin:24px auto}
button{height:90px;font-size:34px;border:0;border-radius:18px;background:#e8551c;color:#fff;touch-action:none}
button.stop{background:#555}
input{width:80%}
</style></head><body>
<h2>TinyBot Remote</h2>
<div class="pad">
<span></span><button data-c="F">▲</button><span></span>
<button data-c="L">◀</button><button class="stop" data-c="S">■</button><button data-c="R">▶</button>
<span></span><button data-c="B">▼</button><span></span>
</div>
<p>Speed <span id="v">180</span></p>
<input type="range" min="100" max="255" value="180" id="spd">
<script>
let timer=null;
function send(c){fetch('/cmd?c='+c+'&s='+document.getElementById('spd').value).catch(()=>{});}
function start(c){stop();send(c);if(c!=='S')timer=setInterval(()=>send(c),200);}
function stop(){if(timer){clearInterval(timer);timer=null;}}
document.querySelectorAll('button').forEach(b=>{
  b.addEventListener('pointerdown',e=>{e.preventDefault();start(b.dataset.c);});
  ['pointerup','pointerleave','pointercancel'].forEach(t=>b.addEventListener(t,()=>{if(timer){stop();send('S');}}));
});
document.getElementById('spd').oninput=e=>document.getElementById('v').textContent=e.target.value;
</script></body></html>
)HTML";

void handleRoot() { server.send_P(200, "text/html", PAGE); }

void handleCommand() {
  String c = server.arg("c");
  if (server.hasArg("s")) speedValue = constrain(server.arg("s").toInt(), 100, 255);
  int s = speedValue;
  int turn = s * 3 / 4;

  if (c == "F")      drive(s, s);
  else if (c == "B") drive(-s, -s);
  else if (c == "L") drive(-turn, turn);
  else if (c == "R") drive(turn, -turn);
  else               drive(0, 0);

  lastCommandMs = millis();
  server.send(200, "text/plain", "ok");
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_IN1, OUTPUT); pinMode(PIN_IN2, OUTPUT);
  pinMode(PIN_IN3, OUTPUT); pinMode(PIN_IN4, OUTPUT);
  ledcAttach(PIN_ENA, PWM_FREQ, PWM_BITS);
  ledcAttach(PIN_ENB, PWM_FREQ, PWM_BITS);
  drive(0, 0);

  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("Hotspot ready. Join it and open http://");
  Serial.println(WiFi.softAPIP());

  server.on("/", handleRoot);
  server.on("/cmd", handleCommand);
  server.begin();
}

void loop() {
  server.handleClient();
  // Failsafe: stop if the phone disconnects or switches apps
  if (lastCommandMs != 0 && millis() - lastCommandMs > FAILSAFE_MS) {
    drive(0, 0);
    lastCommandMs = 0;
  }
}
Download the .ino file

4. Drive it

  1. Upload and open Serial Monitor (115200). You'll see "Hotspot ready".
  2. On your phone, join the TinyBot-Car Wi-Fi.
  3. If the phone warns "no internet", choose stay connected.
  4. Open http://192.168.4.1 and hold the arrows.

5. Troubleshooting

192.168.4.1 won't load

Your phone may have switched back to mobile data. Turn mobile data off, and make sure the address starts with http://, not https.

The car stutters while a button is held

Weak signal or interference. Test within 3 m, or raise FAILSAFE_MS to 1000.

Left and right are swapped

Swap the wires of one motor, or swap the drive() arguments for L and R.

Can it use my home Wi-Fi instead?

Yes: replace WiFi.softAP() with WiFi.begin("ssid", "password") and read the IP from Serial Monitor. Hotspot mode just works anywhere.

Free: ESP32 Robot Wiring Cheat Sheet

One printable page: safe ESP32 pins, L298N, HC-SR04, servo wiring and the most common fixes.

Download the PDF