RoboRacer — Build Your Own Wi-Fi and Remote Controlled Race Car

by sushma_m_09 in Circuits > Arduino

81 Views, 1 Favorites, 0 Comments

RoboRacer — Build Your Own Wi-Fi and Remote Controlled Race Car

RoboRacer — Build Your Own Wi-Fi & Remote Controlled Race Car
20260627_193544.jpg

RoboRacer is a small, fast race car you build yourself. It has four chunky off-road wheels, four powerful motors, and a tiny computer called an ESP32 as its brain.

Here's the fun part: when you switch it on, the car creates its own Wi-Fi network — just like a hotspot. You connect your phone to it, a control screen pops up in your web browser, and you drive using big on-screen buttons. There's nothing to download and no internet needed.

And if you have a Flysky remote control (the kind used for RC cars and drones), you can drive it that way too. The car lets you pick which one you want to use.

By the end of this guide you'll understand:

  1. How a motor driver lets a tiny computer control big motors
  2. How to power electronics safely from a battery
  3. How to upload code to an ESP32
  4. How a web page can become a remote control

Let's go!

Supplies

IMG_4234.JPG

Parts List

- ESP32 WROOM DevKit — 1

- Rhino MDD20A Dual DC Motor Driver (6V–30V) — 1

- Johnson 1000 RPM 12V DC Motors — 4

- Off-road RC Wheels 80mm — 4

- Acrylic Chassis Plate (custom, laser-cut) — 1

- Motor Mount Brackets (metal) — 4

- Brass Shaft Couplers / Wheel Hubs — 4

- DC-DC Buck Converter (12V to 5V) — 1

- 2S–3S LiPo Battery — 1

- XT60 Connector — 1

- Screws, Nuts, Washers — as needed

- Hook-up Wire — as needed

- Flysky FS-i6 Remote + Receiver (optional, for Remote driving) — 1


Tools

- Phillips & flat screwdrivers (small)

- Computer with a USB port

- USB cable for the ESP32

- Small spanner/wrench for wheel nuts (optional)


Safety note for grown-ups & students: This build uses a LiPo battery. LiPo batteries are powerful and must be handled carefully — never short the wires, never charge unattended, and always use the right charger. If you're under 18, do the battery steps with an adult helping.

Meet the Parts (How It All Works)

IMG_4231.JPG
IMG_4224.JPG
IMG_4228.JPG
IMG_4222.JPG
IMG_4218.JPG

Before we build, let's quickly meet the main characters. Understanding what each part does makes the wiring make sense later.

The Brain — ESP32

The ESP32 is a tiny but powerful computer. It does two big jobs in our car: it creates the Wi-Fi network your phone connects to, and it sends "go faster," "turn left," and "stop" signals to the motors. It costs only a few dollars but has Wi-Fi built right in, which is why it's perfect for this.

The Muscle Controller — Motor Driver

The ESP32's brain is smart, but its pins are weak — they can't power a big motor any more than your finger could push a car. The motor driver solves this. Think of it as a power amplifier: the ESP32 whispers "go," and the driver shouts it to the motors using the battery's full power. This board controls two channels (left side and right side), and each channel can run two motors.

The Muscles — Johnson Motors

These four Johnson 1000 RPM motors are strong, fast DC motors. Two power the left wheels and two power the right wheels. By spinning the left and right sides at different speeds, the car can go straight, turn, or spin — this clever trick is called differential drive (the same way a tank steers).

The Wheels

Big, knobbly off-road tires give grip so the car can actually race instead of spinning out.

The Power — Battery + Buck Converter

The LiPo battery is the fuel tank. But there's a catch: the motors want 12 volts, while the ESP32 brain only wants 5 volts. Feeding 12V straight into the ESP32 would fry it. The buck converter is a little voltage "step-down" — it takes the battery's 12V and gently turns it into a safe 5V for the brain.

Building the Chassis

Screenshot 2026-06-29 200549.png
Screenshot 2026-06-29 200220.png
Screenshot 2026-06-30 102130.png
IMG_4216.JPG

3.1 — The Chassis Design

Our chassis is a clear acrylic plate, custom-designed in Onshape (free 3D CAD software) and then laser-cut. Designing it yourself means the holes line up perfectly with your motors and boards.

Don't have a laser cutter? You can cut a chassis from any flat, stiff material — acrylic, plywood, or even thick cardboard for a first test — using these same hole positions.


3.2 — Mount the Motors to the Brackets

Each motor sits inside a square metal bracket that bolts to the chassis.

  1. Place a motor against a bracket so the shaft pokes out through the round hole.
  2. Use the small screws to fix the motor to the bracket.
  3. Repeat for all four motors.


3.3 — Attach the Wheels

  1. Slide a brass coupler onto each motor shaft and tighten its tiny grub screw so it grips.
  2. Push a wheel onto the coupler and secure it.
  3. Give each wheel a spin to check it runs true and doesn't wobble.


3.4 — Bolt the Motors to the Chassis

Attach all four bracket-and-motor assemblies to the corners of the acrylic plate. Make sure:

  1. The two left wheels line up with each other.
  2. The two right wheels line up with each other.
  3. All four wheels touch the ground evenly.

Wiring It All Up

Roboracer Circuit for Wifi + FlySky FS-i6SX.jpg
IMG_4249.JPG

This is the most important step. Go slowly and double-check each connection.

Note: The wiring diagram shows some extra "receiver" pins for the optional Flysky remote. If you're only doing phone control, you can ignore the receiver wires for now and add them later.

We'll wire in four small stages: motors → driver, driver → ESP32, power, and (optional) remote receiver.


4.1 — Motors to the Motor Driver:

Your four motors become two "sides." Wire the two right motors together and the two left motors together, then connect each side to the driver:

- Motors: Right side (2 motors in parallel) | Connect to driver terminal: M1A / M1B

- Motors: Left side (2 motors in parallel) | Connect to driver terminal: M2A / M2B

"In parallel" just means both red wires go to one terminal and both black wires to the other.


4.2 — Motor Driver to the ESP32:

The driver listens to the ESP32 through six wires (plus a shared ground). Connect them exactly like this:

- Driver pin: GND | ESP32 pin: GND | What it controls: Shared ground (very important!)

- Driver pin: PWM1 | ESP32 pin: D25 | What it controls: Right side speed

- Driver pin: DIR1 | ESP32 pin: D26 | What it controls: Right side direction

- Driver pin: PWM2 | ESP32 pin: D27 | What it controls: Left side speed

- Driver pin: DIR2 | ESP32 pin: D33 | What it controls: Left side direction

What do PWM and DIR mean? DIR (direction) tells a motor which way to spin — forward or backward. PWM (speed) tells it how fast, by switching the power on and off very quickly. More "on" time = more speed.


4.3 — Powering Everything:

Here's how the electricity flows, from battery to everything else:

1. The battery plugs in through the XT60 connector.

2. The battery's 12V goes to the motor driver's V+ and V− terminals (this powers the motors).

3. The same 12V also goes into the buck converter's input.

4. The buck converter's 5V output goes to the ESP32's 5V (VIN) and GND pins (this powers the brain).

Important: Before connecting the battery for the first time, double-check that + goes to + and − goes to − everywhere. Reversing power can destroy the ESP32 instantly. Measure with a multimeter if you can.


4.4 — (Optional) The Flysky Receiver:

If you want to drive with the Flysky remote too, connect the receiver's channels to the ESP32:

- Receiver: Channel 1 (steering) signal | ESP32 pin: D12

- Receiver: Channel 2 (throttle) signal | ESP32 pin: D14

- Receiver: Receiver + (VCC) | ESP32 pin: 5V

- Receiver: Receiver − (GND) | ESP32 pin: GND

Installing the Software

Screenshot 2026-06-29 180028.png
downloading-and-installing-img01.png
imgi_11_downloading-and-installing-img02.png
imgi_6_Install-ESP32-Arduino-IDE-2.png

Now we give the car its brain-power by uploading the code.

5.1 — Install the Arduino IDE:

1. Download the Arduino IDE (free) from arduino.cc/en/software and install it.


5.2 — Add ESP32 Support:

The Arduino software doesn't know about the ESP32 yet, so we teach it:

1. Open Arduino IDE → File → Preferences.

2. In "Additional Boards Manager URLs," paste:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

3. Go to Tools → Board → Boards Manager, search for esp32, and install it.

4. Now choose your board: Tools → Board → ESP32 Arduino → DOIT ESP32 DEVKIT V1.


5.3 — Upload the Code:

1. Open the RoboRacer code (below) in the Arduino IDE.

2. Plug the ESP32 into your computer with the USB cable.

3. Select the right port under Tools → Port.

4. Click the Upload button (the arrow). Wait for "Done uploading."


Tip: If upload fails, hold the BOOT button on the ESP32 while it says "Connecting…", then let go.


/*
* ===================================================================
* RoboRacer - Wi-Fi & Remote Controlled Race Car
* ESP32 + Rhino MDD20A dual motor driver + 4x Johnson motors
* ===================================================================
*
* HOW IT WORKS
* ------------
* When the car turns on it makes its own Wi-Fi network called
* "ROBORACER". Connect your phone to it and a control page opens
* at http://192.168.4.1
*
* On that page you choose how to drive:
* - PHONE : drive using the buttons on your phone screen
* - REMOTE : drive using the Flysky FS-i6 remote control
*
* PINS
* ----
* Right motors : PWM -> D25, DIR -> D26 (driver M1)
* Left motors : PWM -> D27, DIR -> D33 (driver M2)
* Flysky CH1 (steer) -> D12
* Flysky CH2 (throttle) -> D14
*
* No extra buttons or LEDs are needed for this build.
* ===================================================================
*/

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>

// ---------------- PINS ----------------
const int CH1_PIN = 12; // Flysky steering
const int CH2_PIN = 14; // Flysky throttle
const int M1_PWM = 25; const int M1_DIR = 26; // right side
const int M2_PWM = 27; const int M2_DIR = 33; // left side

// PWM settings (ESP32 core 3.x style)
const int PWM_FREQ = 5000;
const int PWM_RES = 8;

// ---------------- DRIVE MODES ----------------
// (internal only - the words "mode" never appear on the user's page)
#define DRIVE_PHONE 0
#define DRIVE_RC 1
int driveSource = DRIVE_PHONE;

// ---------------- SAVED SETTINGS ----------------
float speedLimit; // 0.20 .. 1.00
bool invertSteer; // flip turning direction
bool swapMotors; // swap left/right outputs

// ---------------- LIVE STATE ----------------
int phoneMove = 0, phoneTurn = 0; // from phone buttons
unsigned long lastPhoneCmd = 0; // phone failsafe timer
long lastPulse1 = 0, lastPulse2 = 0; // last Flysky readings

WebServer server(80);
Preferences prefs;

// ===================================================================
// MOTORS
// ===================================================================
void motor(int pwmPin, int dirPin, int speed) {
speed = constrain(speed, -255, 255);
if (abs(speed) < 20) {
ledcWrite(pwmPin, 0);
} else {
digitalWrite(dirPin, speed > 0 ? HIGH : LOW);
ledcWrite(pwmPin, abs(speed));
}
}

void stopMotors() {
ledcWrite(M1_PWM, 0);
ledcWrite(M2_PWM, 0);
}

// Turn a forward/back + left/right command into left and right
// wheel speeds, then drive the motors.
void drive(int moveVal, int turnVal) {
int leftSpeed, rightSpeed;
if (!invertSteer) {
leftSpeed = moveVal + turnVal;
rightSpeed = moveVal - turnVal;
} else {
leftSpeed = moveVal - turnVal;
rightSpeed = moveVal + turnVal;
}

leftSpeed *= speedLimit;
rightSpeed *= speedLimit;

if (swapMotors) {
motor(M1_PWM, M1_DIR, leftSpeed);
motor(M2_PWM, M2_DIR, rightSpeed);
} else {
motor(M1_PWM, M1_DIR, rightSpeed);
motor(M2_PWM, M2_DIR, leftSpeed);
}
}

// ===================================================================
// WEB PAGE
// ===================================================================
void handleRoot() {
String h = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no'>";
h += "<title>RoboRacer</title><style>";
h += ":root{--bg:#f4f6f8;--card:#fff;--ink:#1c2530;--ink-soft:#67727f;--line:#e3e8ee;--brand:#ff5a3c;--brand-dk:#e34324;--go:#1fb364;--shadow:0 6px 22px rgba(28,37,48,.08);}";
h += "*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;margin:0;padding:0;}";
h += "body{font-family:'Trebuchet MS','Verdana',sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;padding:22px 18px 36px;background-image:radial-gradient(circle at 18% 0%,rgba(255,90,60,.07),transparent 45%);}";
h += ".wrap{max-width:440px;margin:auto;}";
h += ".head{display:flex;align-items:center;gap:12px;margin-bottom:22px;}";
h += ".logo{width:46px;height:46px;border-radius:13px;background:linear-gradient(135deg,var(--brand),var(--brand-dk));display:flex;align-items:center;justify-content:center;box-shadow:var(--shadow);flex-shrink:0;}";
h += ".logo svg{width:28px;height:28px;fill:#fff;}";
h += ".title h1{font-size:21px;letter-spacing:.5px;line-height:1;}.title span{font-size:11px;color:var(--ink-soft);letter-spacing:2px;text-transform:uppercase;}";
h += ".card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:20px;box-shadow:var(--shadow);margin-bottom:16px;}";
h += ".lead{font-size:14px;color:var(--ink-soft);margin-bottom:16px;line-height:1.5;}";
h += ".choose{display:grid;grid-template-columns:1fr 1fr;gap:12px;}";
h += ".opt{border:2px solid var(--line);border-radius:15px;padding:18px 12px;text-align:center;cursor:pointer;transition:.16s;background:#fff;}";
h += ".opt:active{transform:scale(.97);}.opt.on{border-color:var(--brand);background:#fff6f4;box-shadow:0 0 0 4px rgba(255,90,60,.08);}";
h += ".opt .ic{width:44px;height:44px;margin:0 auto 9px;border-radius:12px;background:#f0f3f6;display:flex;align-items:center;justify-content:center;}";
h += ".opt.on .ic{background:var(--brand);}";
h += ".opt .ic svg{width:24px;height:24px;stroke:var(--ink-soft);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}.opt.on .ic svg{stroke:#fff;}";
h += ".opt b{display:block;font-size:15px;margin-bottom:2px;}.opt small{font-size:11px;color:var(--ink-soft);line-height:1.3;display:block;}";
h += ".go{display:block;width:100%;border:none;border-radius:14px;padding:16px;margin-top:16px;background:var(--go);color:#fff;font-size:16px;font-weight:bold;letter-spacing:.5px;cursor:pointer;box-shadow:0 5px 16px rgba(31,179,100,.28);transition:.15s;font-family:inherit;}";
h += ".go:active{transform:scale(.98);}";
h += ".hidden{display:none;}";
h += ".bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}";
h += ".back{display:flex;align-items:center;gap:6px;color:var(--ink-soft);font-size:13px;cursor:pointer;background:#fff;border:1px solid var(--line);padding:8px 13px;border-radius:11px;}";
h += ".back svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;}";
h += ".pill{font-size:12px;font-weight:bold;color:var(--brand);background:#fff6f4;border:1px solid #ffd9cf;padding:7px 13px;border-radius:20px;letter-spacing:.5px;}";
h += ".pad{display:grid;grid-template-columns:repeat(3,1fr);gap:11px;max-width:300px;margin:6px auto 0;}";
h += ".key{aspect-ratio:1;border:1px solid var(--line);background:#fff;border-radius:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none;box-shadow:var(--shadow);transition:.1s;}";
h += ".key svg{width:40px;height:40px;stroke:var(--brand);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round;}";
h += ".key.act{background:var(--brand);transform:scale(.95);}.key.act svg{stroke:#fff;}";
h += ".key.stop{font-weight:bold;color:#fff;background:var(--ink);font-size:15px;letter-spacing:1px;border:none;}.key.stop.act{background:#000;}";
h += ".blank{visibility:hidden;}";
h += ".speedcard{margin-top:16px;}.speedrow{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:10px;}.speedrow .v{font-size:26px;font-weight:bold;color:var(--brand);}";
h += ".slider{width:100%;height:8px;border-radius:5px;background:var(--line);outline:none;-webkit-appearance:none;}";
h += ".slider::-webkit-slider-thumb{-webkit-appearance:none;width:28px;height:28px;border-radius:50%;background:var(--brand);cursor:pointer;box-shadow:0 3px 8px rgba(255,90,60,.4);border:4px solid #fff;}";
h += ".slider::-moz-range-thumb{width:28px;height:28px;border-radius:50%;background:var(--brand);border:4px solid #fff;cursor:pointer;}";
h += ".info{text-align:center;padding:10px 0;}.info .big{width:66px;height:66px;margin:0 auto 14px;border-radius:18px;background:#f0f3f6;display:flex;align-items:center;justify-content:center;}";
h += ".info .big svg{width:36px;height:36px;stroke:var(--brand);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}";
h += ".info h2{font-size:18px;margin-bottom:8px;}.info p{font-size:13px;color:var(--ink-soft);line-height:1.6;max-width:300px;margin:0 auto;}";
h += ".steps{text-align:left;margin:16px auto 0;max-width:300px;padding-left:20px;}.steps li{font-size:13px;color:var(--ink-soft);margin-bottom:8px;line-height:1.4;}";
h += ".tune{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px;}";
h += ".tg{text-align:center;padding:11px 6px;border-radius:12px;border:1px solid var(--line);background:#fff;color:var(--ink);cursor:pointer;font-size:12px;font-weight:bold;}";
h += ".tg small{display:block;color:var(--ink-soft);font-weight:normal;margin-top:3px;}";
h += ".tg.on{border-color:var(--brand);color:var(--brand);background:#fff6f4;}.tg.on small{color:var(--brand);}";
h += ".foot{text-align:center;font-size:11px;color:var(--ink-soft);margin-top:18px;letter-spacing:.5px;}";
h += "</style></head><body><div class='wrap'>";

// header
h += "<div class='head'><div class='logo'><svg viewBox='0 0 24 24'><path d='M5 11l1.5-4A2 2 0 0 1 8.4 6h7.2a2 2 0 0 1 1.9 1.3L19 11h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1.2a2.5 2.5 0 0 1-4.6 0H9.8a2.5 2.5 0 0 1-4.6 0H4a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1z'/></svg></div>";
h += "<div class='title'><h1>RoboRacer</h1><span>Wi-Fi Race Car</span></div></div>";

// HOME
h += "<div id='home'><div class='card'><p class='lead'>Welcome! Choose how you want to drive your car:</p><div class='choose'>";
h += "<div class='opt on' id='optPhone' onclick=\"pick('phone')\"><div class='ic'><svg viewBox='0 0 24 24'><rect x='6' y='2' width='12' height='20' rx='3'/><line x1='11' y1='18' x2='13' y2='18'/></svg></div><b>Phone</b><small>Drive with on-screen buttons</small></div>";
h += "<div class='opt' id='optRc' onclick=\"pick('rc')\"><div class='ic'><svg viewBox='0 0 24 24'><rect x='4' y='9' width='16' height='12' rx='2'/><line x1='8' y1='3' x2='8' y2='9'/><line x1='16' y1='3' x2='16' y2='9'/><circle cx='9' cy='14' r='1.3'/><circle cx='15' cy='16' r='1.3'/></svg></div><b>Remote</b><small>Use the Flysky controller</small></div>";
h += "</div><button class='go' id='goBtn' onclick='start()'>START DRIVING</button></div>";
h += "<div class='foot'>Connected to ROBORACER &nbsp;&bull;&nbsp; 192.168.4.1</div></div>";

// PHONE SCREEN
h += "<div id='phoneScreen' class='hidden'><div class='bar'><div class='back' onclick='goHome()'><svg viewBox='0 0 24 24'><polyline points='15 18 9 12 15 6'/></svg>Back</div><div class='pill'>PHONE DRIVE</div></div>";
h += "<div class='card'><div class='pad'>";
h += "<div class='blank'></div><div class='key' data-c='F'><svg viewBox='0 0 24 24'><polyline points='6 15 12 9 18 15'/></svg></div><div class='blank'></div>";
h += "<div class='key' data-c='L'><svg viewBox='0 0 24 24'><polyline points='15 6 9 12 15 18'/></svg></div><div class='key stop' data-c='S'>STOP</div><div class='key' data-c='R'><svg viewBox='0 0 24 24'><polyline points='9 6 15 12 9 18'/></svg></div>";
h += "<div class='blank'></div><div class='key' data-c='B'><svg viewBox='0 0 24 24'><polyline points='6 9 12 15 18 9'/></svg></div><div class='blank'></div>";
h += "</div></div>";
h += "<div class='card speedcard'><div class='speedrow'><b>Speed Limit</b><span class='v' id='spd'>" + String(speedLimit*100,0) + "%</span></div>";
h += "<input type='range' min='20' max='100' value='" + String(speedLimit*100,0) + "' class='slider' id='slider'></div>";
// tuning (kept simple, shown under speed)
h += "<div class='card'><b style='font-size:13px;'>If the car drives the wrong way, tap to fix:</b><div class='tune'>";
h += "<div class='tg " + String(invertSteer?"on":"") + "' id='tgSteer' onclick='tSteer()'>TURNING<small id='tgSteerL'>" + String(invertSteer?"Flipped":"Normal") + "</small></div>";
h += "<div class='tg " + String(swapMotors?"on":"") + "' id='tgSwap' onclick='tSwap()'>LEFT/RIGHT<small id='tgSwapL'>" + String(swapMotors?"Swapped":"Normal") + "</small></div>";
h += "</div></div></div>";

// RC SCREEN
h += "<div id='rcScreen' class='hidden'><div class='bar'><div class='back' onclick='goHome()'><svg viewBox='0 0 24 24'><polyline points='15 18 9 12 15 6'/></svg>Back</div><div class='pill'>REMOTE DRIVE</div></div>";
h += "<div class='card'><div class='info'><div class='big'><svg viewBox='0 0 24 24'><rect x='4' y='9' width='16' height='12' rx='2'/><line x1='8' y1='3' x2='8' y2='9'/><line x1='16' y1='3' x2='16' y2='9'/><circle cx='9' cy='14' r='1.3'/><circle cx='15' cy='16' r='1.3'/></svg></div>";
h += "<h2>Remote Control Ready!</h2><p>Your car is now listening to the Flysky remote. You can put your phone away and drive with the controller.</p>";
h += "<ol class='steps'><li>Turn on your Flysky remote.</li><li>Right stick up / down = forward / back.</li><li>Left stick left / right = turn.</li></ol>";
h += "</div></div></div>";

// ---------------- JS ----------------
h += "<script>";
h += "function $(i){return document.getElementById(i);}";
h += "let mode='phone';";
h += "function pick(m){mode=m;$('optPhone').classList.toggle('on',m=='phone');$('optRc').classList.toggle('on',m=='rc');}";
h += "function start(){fetch('/source?s='+(mode=='rc'?'rc':'phone'));$('home').classList.add('hidden');";
h += "if(mode=='phone')$('phoneScreen').classList.remove('hidden');else $('rcScreen').classList.remove('hidden');}";
h += "function goHome(){fetch('/drive?c=S');$('home').classList.remove('hidden');$('phoneScreen').classList.add('hidden');$('rcScreen').classList.add('hidden');}";
// speed slider -> /set
h += "let st;var sl=$('slider');if(sl){sl.oninput=function(){$('spd').textContent=this.value+'%';var v=this.value;clearTimeout(st);st=setTimeout(function(){fetch('/set?speed='+v);},120);};}";
// tuning toggles
h += "var iS=" + String(invertSteer?"true":"false") + ",sM=" + String(swapMotors?"true":"false") + ";";
h += "function tSteer(){iS=!iS;fetch('/set?steer='+iS);$('tgSteer').classList.toggle('on',iS);$('tgSteerL').textContent=iS?'Flipped':'Normal';}";
h += "function tSwap(){sM=!sM;fetch('/set?swap='+sM);$('tgSwap').classList.toggle('on',sM);$('tgSwapL').textContent=sM?'Swapped':'Normal';}";
// drive keys: repeat-send while held so the car keeps going
h += "document.querySelectorAll('.key').forEach(function(k){var rep=null;";
h += "var go=function(e){e.preventDefault();if(rep)return;k.classList.add('act');var c=k.dataset.c;fetch('/drive?c='+c);";
h += "if(c!='S'){rep=setInterval(function(){fetch('/drive?c='+c);},150);}};";
h += "var end=function(){k.classList.remove('act');if(rep){clearInterval(rep);rep=null;}if(k.dataset.c!='S')fetch('/drive?c=S');};";
h += "k.addEventListener('touchstart',go);k.addEventListener('mousedown',go);";
h += "k.addEventListener('touchend',end);k.addEventListener('mouseup',end);k.addEventListener('mouseleave',end);k.addEventListener('touchcancel',end);});";
h += "window.addEventListener('blur',function(){fetch('/drive?c=S');});";
h += "</script></body></html>";

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

// pick phone or rc as the active driving source
void handleSource() {
if (server.hasArg("s")) {
driveSource = (server.arg("s") == "rc") ? DRIVE_RC : DRIVE_PHONE;
stopMotors();
phoneMove = phoneTurn = 0;
}
server.send(200, "text/plain", "ok");
}

// phone button command
void handleDrive() {
if (server.hasArg("c")) {
char c = server.arg("c")[0];
int s = 220;
switch (c) {
case 'F': phoneMove = s; phoneTurn = 0; break;
case 'B': phoneMove = -s; phoneTurn = 0; break;
case 'L': phoneMove = 0; phoneTurn = -s; break;
case 'R': phoneMove = 0; phoneTurn = s; break;
default: phoneMove = 0; phoneTurn = 0; break;
}
lastPhoneCmd = millis();
}
server.send(200, "text/plain", "ok");
}

// live settings
void handleSet() {
if (server.hasArg("speed")) {
speedLimit = server.arg("speed").toFloat() / 100.0;
prefs.putFloat("speed", speedLimit);
}
if (server.hasArg("steer")) {
invertSteer = (server.arg("steer") == "true");
prefs.putBool("steer", invertSteer);
}
if (server.hasArg("swap")) {
swapMotors = (server.arg("swap") == "true");
prefs.putBool("swap", swapMotors);
}
server.send(200, "text/plain", "ok");
}

// ===================================================================
// SETUP
// ===================================================================
void setup() {
Serial.begin(115200);

prefs.begin("roboracer", false);
speedLimit = prefs.getFloat("speed", 0.8);
invertSteer = prefs.getBool("steer", false);
swapMotors = prefs.getBool("swap", false);

pinMode(CH1_PIN, INPUT);
pinMode(CH2_PIN, INPUT);
pinMode(M1_DIR, OUTPUT);
pinMode(M2_DIR, OUTPUT);
ledcAttach(M1_PWM, PWM_FREQ, PWM_RES);
ledcAttach(M2_PWM, PWM_FREQ, PWM_RES);
stopMotors();

// Make the car's own Wi-Fi network
WiFi.mode(WIFI_AP);
WiFi.softAP("ROBORACER", "12345678");
Serial.print("Open this address on your phone: ");
Serial.println(WiFi.softAPIP());

server.on("/", handleRoot);
server.on("/source", handleSource);
server.on("/drive", handleDrive);
server.on("/set", handleSet);
server.begin();
}

// ===================================================================
// LOOP
// ===================================================================
void loop() {
server.handleClient();

if (driveSource == DRIVE_RC) {
// ----- Flysky remote -----
long rawSteer = pulseIn(CH1_PIN, HIGH, 25000);
long rawThrottle = pulseIn(CH2_PIN, HIGH, 25000);
if (rawSteer > 0) lastPulse1 = rawSteer;
if (rawThrottle > 0) lastPulse2 = rawThrottle;

if (rawThrottle > 900 && rawSteer > 900) {
int moveVal = map(rawThrottle, 1000, 2000, -255, 255);
int turnVal = map(rawSteer, 1000, 2000, -255, 255);
drive(moveVal, turnVal);
} else {
stopMotors(); // no remote signal -> stop
}
} else {
// ----- Phone buttons -----
if (millis() - lastPhoneCmd > 500) { phoneMove = 0; phoneTurn = 0; }
drive(phoneMove, phoneTurn);
}
}

Downloads

Your First Drive!

RoboRacer &mdash; Build Your Own Wi-Fi &amp; Remote Controlled Race Car

The exciting part. Let's drive.

▶ Watch the RoboRacer build & test drive on YouTube

  1. Power on the car by connecting the battery.
  2. On your phone, open Wi-Fi settings and connect to the network called ROBORACER. The password is 12345678.
  3. Your phone may say "this network has no internet" — that's normal! Tap "Stay connected" or "Keep".
  4. Open any web browser (Chrome, Safari) and type 192.168.4.1 into the address bar.
  5. The RoboRacer control screen appears!

Choose How to Drive

The screen gives you two choices:

  1. Phone — drive using the big arrow buttons right on your screen. Press and hold to move; let go to stop.
  2. Remote — switch to driving with the Flysky controller.

Tap your choice, then tap START DRIVING.

Troubleshooting & Going Further

If something's not right:

- Problem: Car drives the wrong way when I press a button | Likely fix: On the drive screen, tap TURNING to flip the steering, or LEFT/RIGHT to swap the sides.

- Problem: One side doesn't move | Likely fix: Check that side's motor wires at the driver terminals — a loose wire is the usual cause.

- Problem: The control page won't open | Likely fix: Make sure you tapped "Stay connected" on the Wi-Fi prompt, and typed 192.168.4.1 in the address bar, not a search box.

- Problem: Car spins in a circle instead of going straight | Likely fix: The two sides are fighting — tap LEFT/RIGHT on the screen, or check that left motors are on M2 and right motors on M1.

- Problem: Nothing powers on | Likely fix: Check the battery charge and the XT60 connection. Check + and − aren't reversed.

- Problem: Motors are weak / car is slow | Likely fix: Battery may be low, or the speed slider is turned down.


Make It Your Own:

- Race your friends — build two and time laps.

- Add lights — wire up some LEDs for night racing.

- Change the look — design your own chassis in Onshape.

- Tune the speed — experiment with the speed limit for control vs. top speed.


You Did It! 🏁

You've built a real, working, remote-controlled racing robot — and learned how brains, muscles, and power come together in every robot. The same ideas (a controller, a motor driver, and a power supply) are inside everything from toy cars to Mars rovers.

Now go race!


If you make one, share a photo — we'd love to see it!