Project Spec Sheet
Difficulty: Intermediate (Requires soldering and basic C++ logic)
Time to Build: 4–6 hours
Estimated Cost: $35–$45 USD
Target Board: ESP32-WROOM-32 DevKit V1 (30-pin variant)

When judges evaluate science fair robotics projects, they look past the plastic chassis and focus on data, methodology, and problem-solving. A standard Arduino Uno line-follower is a great weekend build, but it lacks the telemetry and processing power to impress at a regional or state-level science fair. By upgrading to an ESP32, you gain dual-core processing and native WiFi, allowing your robot to stream lap times, sensor thresholds, and battery voltage to a live dashboard in real-time.

This guide walks you through building a high-performance, telemetry-enabled line-following rover. We will cover the exact hardware stack, the proportional-integral-derivative (PID) inspired control code, and the specific hardware traps that cause most student builds to fail on presentation day.

Why the ESP32 Dominates Science Fair Robotics Projects

The Society for Science, which runs the Regeneron ISEF, heavily weights projects that demonstrate rigorous data collection. The ESP32-WROOM-32 DevKit V1 (30-pin) is the ideal brain for this. Unlike the ATmega328P found in the Arduino Uno, the ESP32 features a 12-bit ADC (analog-to-digital converter), allowing you to read exact millivolt values from your IR sensors rather than simple HIGH/LOW digital states. This means your robot can calculate the exact center of the line using a weighted average, resulting in buttery-smooth cornering instead of the aggressive left-right jitter typical of basic builds.

Furthermore, the ESP32’s WiFi radio allows you to implement MQTT or simple HTTP GET requests to log every lap time and sensor reading to a cloud spreadsheet. Presenting a live graph of your robot's steering corrections to the judges is a massive differentiator in the engineering category.

Hardware Spec Sheet & Pin Mapping

Do not buy generic "smart car kits" on Amazon; the included TT motors often have mismatched gear ratios that cause the robot to pull to one side. Source these specific components individually.

Component Exact Variant / Model Est. Cost Notes
Microcontroller ESP32-WROOM-32 DevKit V1 (30-pin) $6.00 Ensure it has the CP2102 or CH340 USB-UART chip.
Motor Driver L298N Dual H-Bridge Module $4.50 Must include the heatsink. Remove the 5V jumper cap.
Sensors TCRT5000 Reflective Optical (3-pack) $5.00 Use analog output (A0) pins, not digital (D0).
Motors TT Gear Motors (3-6V, 200 RPM) $8.00 Buy from a reputable robotics shop (e.g., Pololu or Adafruit) to ensure matched gearing.
Power 2x 18650 Li-ion cells + 2S BMS $12.00 7.4V nominal. Never use unprotected cells in series.
Regulator LM2596 Buck Converter Module $2.50 Set to exactly 5.0V to power the ESP32.

ESP32 Pin Mapping Table

ESP32 GPIO Target Component Function
GPIO 34Left TCRT5000 (AO)Analog Input (Input only pin)
GPIO 35Center TCRT5000 (AO)Analog Input (Input only pin)
GPIO 32Right TCRT5000 (AO)Analog Input
GPIO 27L298N IN1Digital Output (Left Motor Dir)
GPIO 26L298N IN2Digital Output (Left Motor Dir)
GPIO 25L298N IN3Digital Output (Right Motor Dir)
GPIO 33L298N IN4Digital Output (Right Motor Dir)
GPIO 14L298N ENAPWM Output (Left Motor Speed)
GPIO 12L298N ENBPWM Output (Right Motor Speed)

Step-by-Step Assembly & Power Safety

⚠️ Lithium-Ion Safety Warning: You are wiring two 18650 cells in series (7.4V nominal, 8.4V fully charged). A short circuit here can cause a thermal runaway fire. Always use a 2S BMS (Battery Management System) board to protect against over-discharge and short circuits. Never solder directly to battery terminals; use a spot-welded nickel strip or a pre-tabbed cell holder.
  1. Prep the Power Rail: Wire your 2S battery pack to the BMS, then route the main P+ and P- pads to the L298N’s 12V and GND screw terminals. (The L298N handles up to 35V peak, so 8.4V is perfectly safe).
  2. Configure the Buck Converter: Before connecting the ESP32, power the LM2596 buck converter from the L298N's 5V and GND outputs. Use a multimeter to turn the tiny brass potentiometer screw until the output reads exactly 5.0V. Once set, wire this 5V output to the ESP32’s 5V and GND pins.
  3. Remove the L298N 5V Jumper: The onboard 7805 linear regulator on cheap L298N modules cannot supply the 240mA+ current spikes the ESP32 draws when transmitting over WiFi. Leaving the jumper on will cause severe brownouts. Power the ESP32 exclusively from the buck converter.
  4. Mount the Sensors: Zip-tie or bolt the three TCRT5000 sensors to the front of the chassis. The center sensor must be exactly on the robot's centerline. The left and right sensors should be spaced 2.5 cm apart. Keep them 1.0 cm to 1.5 cm above the floor for optimal IR reflection.
  5. Wire the Logic: Connect the ESP32 GPIO pins to the L298N IN1-IN4 and ENA/ENB pins according to the mapping table above. Ensure all grounds (Battery, L298N, Buck Converter, ESP32) are tied together in a common star-ground topology to prevent ground loops.

Complete ESP32 Line-Follower Code

This code uses a weighted-average steering algorithm. Instead of simple bang-bang control (hard left/hard right), it calculates an error value based on how far the line is from the center sensor and applies proportional braking to the inside wheel. This targets the ESP32-WROOM-32 DevKit V1 and uses the native ledc API for reliable PWM generation across all ESP32 Arduino Core versions.

#include <Arduino.h>

// --- PIN DEFINITIONS ---
#define SENSOR_LEFT   34
#define SENSOR_CENTER 35
#define SENSOR_RIGHT  32

#define IN1 27 // Left Dir
#define IN2 26 // Left Dir
#define IN3 25 // Right Dir
#define IN4 33 // Right Dir
#define ENA 14 // Left PWM
#define ENB 12 // Right PWM

// --- PWM CONFIGURATION (ESP32 LEDC API) ---
#define PWM_FREQ 5000
#define PWM_RES 8
#define CH_ENA 0
#define CH_ENB 1

// --- TUNING PARAMETERS ---
const int BASE_SPEED = 160;   // 0-255 scale
const float KP = 1.2;         // Proportional constant (tune this!)
const int SENSOR_THRESHOLD = 2500; // 12-bit ADC max is 4095

void setup() {
  Serial.begin(115200);
  
  // Motor direction pins
  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  
  // Configure LEDC PWM channels
  ledcSetup(CH_ENA, PWM_FREQ, PWM_RES);
  ledcSetup(CH_ENB, PWM_FREQ, PWM_RES);
  ledcAttachPin(ENA, CH_ENA);
  ledcAttachPin(ENB, CH_ENB);
  
  // Stop motors initially
  ledcWrite(CH_ENA, 0);
  ledcWrite(CH_ENB, 0);
  
  Serial.println("ESP32 Telemetry Rover Initialized. Calibrating...");
  delay(1000);
}

void loop() {
  // Read 12-bit ADC values (Higher value = darker surface / line detected)
  int leftVal = analogRead(SENSOR_LEFT);
  int centerVal = analogRead(SENSOR_CENTER);
  int rightVal = analogRead(SENSOR_RIGHT);
  
  // Calculate weighted error (-1.0 to 1.0)
  // If line is on left, leftVal is high, error becomes negative (turn left)
  float error = (float)(rightVal - leftVal) / (float)(leftVal + centerVal + rightVal + 1);
  
  // Proportional steering calculation
  float steering = error * KP;
  
  int leftSpeed = BASE_SPEED + (steering * BASE_SPEED);
  int rightSpeed = BASE_SPEED - (steering * BASE_SPEED);
  
  // Constrain speeds to 0-255 PWM limits
  leftSpeed = constrain(leftSpeed, 0, 255);
  rightSpeed = constrain(rightSpeed, 0, 255);
  
  // Apply motor directions and speeds
  driveMotor(true, leftSpeed);  // Left motor
  driveMotor(false, rightSpeed); // Right motor
  
  // Telemetry output for serial plotter / WiFi logging
  Serial.printf("L:%d C:%d R:%d | Err:%.2f | Spd:%d,%d\n", 
                leftVal, centerVal, rightVal, error, leftSpeed, rightSpeed);
}

void driveMotor(bool isLeft, int speed) {
  uint8_t ch = isLeft ? CH_ENA : CH_ENB;
  uint8_t dir1 = isLeft ? IN1 : IN3;
  uint8_t dir2 = isLeft ? IN2 : IN4;
  
  if (speed >= 0) {
    digitalWrite(dir1, HIGH);
    digitalWrite(dir2, LOW);
  } else {
    digitalWrite(dir1, LOW);
    digitalWrite(dir2, HIGH);
    speed = -speed;
  }
  ledcWrite(ch, speed);
}

Debugging: Boot Failures and Sensor Drift

When presenting at a science fair, ambient lighting and booth vibrations will test your build. Here is how to handle the most common points of failure.

The "Failed to Connect" Upload Error

If you are trying to flash the code and the Arduino IDE hangs at "Connecting..." before throwing this exact string:

A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header

Here are the first three things to check, ranked by likelihood:

  1. Check the USB Cable Type: 60% of the time, you are using a charge-only micro-USB cable. Swap to a verified data-sync cable. If the PC doesn't chime when you plug it in, it's the cable.
  2. Force Bootloader Mode (GPIO0): Some DevKit clones lack the auto-reset circuit. Press and hold the BOOT button on the ESP32, click "Upload" in the IDE, and release the BOOT button only when the IDE says "Connecting...".
  3. Verify the UART Driver: Check your Windows Device Manager under "Ports (COM & LPT)". If you see an unknown device or a yellow triangle, you need to install the CH340 or CP2102 driver, depending on the square black chip located next to the USB port on your board.

Sensor Drift Under Halogen Lights

Science fair gyms are often lit with harsh, flickering halogen or LED overheads. The TCRT5000 IR sensors can become saturated, reading a "dark" value even when over a white floor. The fix: Add a physical shroud (a small piece of black heat-shrink tubing or 3D printed hood) over each IR LED and phototransistor. In code, implement a startup calibration routine that reads the ambient white floor value and dynamically sets the SENSOR_THRESHOLD variable.

How to Extend or Simplify the Build

Depending on your grade level and time remaining before the fair, you can scale this project up or down.

To Simplify (Middle School / Beginners):
Swap the ESP32 for an Arduino Nano v3. Change the analog reads to use the digital D0 pins on the TCRT5000 modules (using the onboard potentiometer to set a hard threshold). Replace the ledc PWM code with standard analogWrite(). You lose WiFi telemetry, but the C++ logic becomes much easier to explain to judges.

To Extend (High School / Advanced):
Add an MPU6050 IMU via I2C (GPIO 21 for SDA, GPIO 22 for SCL). Use the yaw data to implement a PID loop that keeps the robot driving perfectly straight on dashed lines where the IR sensors temporarily lose the track. Furthermore, use the ESP-IDF WiFi libraries to push the telemetry data via MQTT to a Raspberry Pi running Node-RED, displaying a real-time steering graph on a monitor at your booth.

Science Fair Robotics Projects FAQ

What are the best science fair robotics projects for middle school?

For middle school, the best projects isolate a single variable to test. Instead of just "building a robot that follows a line," build a robot that tests how different tire treads affect cornering slip, or how battery voltage sag impacts lap times. The robot is just the tool; the scientific method and data collection are the actual project. Keep the electronics simple (Arduino Nano + digital sensors) so you can focus on the experimental design.

How do I add wireless data logging to my science fair robot?

Using the ESP32, the easiest method is to format your telemetry data as a CSV string and send it via an HTTP POST request to a free service like ThingSpeak or a custom Google Apps Script tied to a Google Sheet. In your Arduino loop, trigger the WiFi transmission only once every 500ms to prevent network latency from interrupting your motor control loop. Run the motor logic on Core 1, and the WiFi logging on Core 0 using FreeRTOS tasks.

Why does my line-following robot jitter on sharp turns?

Jitter is almost always caused by "bang-bang" control logic (where the robot goes 100% left or 100% right) combined with a sensor polling rate that is too slow. To fix this, implement the proportional steering algorithm provided in the code above. By calculating a weighted error and applying a proportional braking force to the inside wheel (rather than reversing it), the robot will carve through sharp 90-degree turns smoothly. Ensure your loop() executes in under 5 milliseconds.