ESP32 Line Following Robot

RoboticsBeginnerIntermediateAdvanced

Program an ESP32-powered two-wheel robot that autonomously follows a black tape line using infrared sensors, from basic threshold logic up to smooth PID control with Bluetooth override.

Overview

In this beginner project you will build a two-wheel robot car that follows a black tape line on a white floor. Two infrared proximity sensors detect the line position, and an L298N dual H-bridge module drives two DC motors. When both sensors detect white the robot drives forward. When one sensor detects black it turns to re-centre on the line. This project teaches GPIO digital reads, motor direction control, and simple conditional logic with no external libraries required.

Components
  • 1× ESP32 DevKit V1 — 30-pin or 38-pin
  • 1× L298N dual H-bridge motor driver module — Onboard 5 V regulator
  • 2× TCRT5000 IR line sensor module — With sensitivity potentiometer
  • 2× TT DC gear motor with wheel — 3-6 V, 200 RPM
  • 1× 2WD robot chassis — Acrylic or 3D-printed
  • 1× 7.4 V 1200 mAh Li-Po battery — Powers motors and ESP32
  • 1× Breadboard and jumper wires — M-M and M-F
Wiring
Component PinESP32 PinNotes
L298N IN1GPIO 25Left motor direction A
L298N IN2GPIO 26Left motor direction B
L298N IN3GPIO 27Right motor direction A
L298N IN4GPIO 14Right motor direction B
Left IR OUTGPIO 34Input-only; LOW = black detected
Right IR OUTGPIO 35Input-only; LOW = black detected
L298N ENA5 V jumperKeep jumper for full speed
L298N ENB5 V jumperKeep jumper for full speed
L298N 12 V terminalBattery +Li-Po 7.4 V
L298N GNDCommon GNDShared with ESP32 GND
Arduino Code
esp32-line-following-robot_beginner.ino
// ESP32 Line Following Robot - Beginner
// Two IR sensors + L298N motor driver, no libraries needed

const int IN1 = 25, IN2 = 26; // left motor
const int IN3 = 27, IN4 = 14; // right motor
const int IR_L = 34, IR_R = 35;

void forward()   { digitalWrite(IN1,HIGH);digitalWrite(IN2,LOW);
                   digitalWrite(IN3,HIGH);digitalWrite(IN4,LOW); }
void turnLeft()  { digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH);
                   digitalWrite(IN3,HIGH);digitalWrite(IN4,LOW); }
void turnRight() { digitalWrite(IN1,HIGH);digitalWrite(IN2,LOW);
                   digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); }
void stopAll()   { digitalWrite(IN1,LOW);digitalWrite(IN2,LOW);
                   digitalWrite(IN3,LOW);digitalWrite(IN4,LOW); }

void setup() {
  pinMode(IN1,OUTPUT);pinMode(IN2,OUTPUT);
  pinMode(IN3,OUTPUT);pinMode(IN4,OUTPUT);
  pinMode(IR_L,INPUT);
  pinMode(IR_R,INPUT);
  Serial.begin(115200);
}

void loop() {
  int L = digitalRead(IR_L);
  int R = digitalRead(IR_R);
  // HIGH = white surface, LOW = black line
  if (L == HIGH && R == HIGH) { forward();   }
  else if (L == LOW  && R == HIGH) { turnLeft();  }
  else if (L == HIGH && R == LOW)  { turnRight(); }
  else                             { stopAll();   }
  delay(10);
}
How It Works
01

IR Reflection Principle: Each TCRT5000 emits infrared light downward. Black tape absorbs IR so the receiver sees less light and the OUT pin goes LOW. White surface reflects strongly, keeping OUT HIGH.

02

Conditional Direction Logic: The ESP32 reads both sensor pins every 10 ms. Four conditions map to forward, left-turn, right-turn, and stop, covering every sensor combination.

03

H-Bridge Motor Control: The L298N H-bridge reverses polarity to each motor pair by toggling IN1/IN2 and IN3/IN4. HIGH on IN1 and LOW on IN2 spins the left motor forward; swapping reverses it.

04

Sensitivity Adjustment: The potentiometer on each IR module sets the detection threshold. Turn it until the onboard LED lights on white and extinguishes on the black tape at operating height.

Applications
  • Warehouse floor logistics following tape guides
  • Classroom STEM robotics demonstrations
  • Automated conveyor routing prototype
  • Entry-level robotics competition platform
Troubleshooting

Robot ignores the line and drives straight

Adjust the IR sensor potentiometers. The onboard LED must be ON over white and OFF over black tape at the actual mounting height (10-15 mm).

Motors spin but chassis does not move

Verify ENA and ENB jumpers are installed on the L298N. Check that motor wires are firmly inserted in the terminal block with correct polarity.

Robot oscillates back and forth on the line

Widen the sensor spacing or reduce motor speed by adding a short delay after each turn command. The intermediate level adds PID for smooth correction.

ESP32 resets when motors start

Motor inrush current causes a voltage dip. Add a 100 uF capacitor across the 5 V rail and power the ESP32 from the L298N 5 V output, not directly from the battery.

Upgrades
  • Remove ENA/ENB jumpers and add PWM pins for variable speed control
  • Add an HC-SR04 ultrasonic sensor for obstacle avoidance
  • Add an OLED to display current direction and sensor states
  • Add a push button to start or stop the robot without power cycling
FAQ

You need an ESP32 DevKit, TODO: sensor, L298N IN1, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Robotics. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The intermediate build replaces the two-sensor binary system with a five-sensor analog array and a PID controller that produces smooth, proportional speed corrections. LEDC PWM on ENA and ENB allows variable speed so the robot accelerates on straights and automatically slows through curves. Calibrated thresholds are stored in NVS flash so the robot adapts to different lighting environments without reflashing.

Components
  • 1× ESP32 DevKit V1
  • 1× 5-element TCRT5000 sensor array — Pre-assembled bar module
  • 1× L298N motor driver — ENA/ENB will be PWM-driven
  • 2× DC gear motor 6 V 200 RPM — Higher torque TT-motors
  • 1× 7.4 V 2000 mAh Li-Po
  • 1× SSD1306 OLED 128x64 I2C — Shows mode and PID error
  • 1× Tactile push button — Calibration trigger
Wiring
Component PinESP32 PinNotes
Sensor S0-S4 (analog)GPIO 36,39,34,35,32ADC-capable input-only pins
L298N IN1-IN4GPIO 25,26,27,14Same as beginner level
L298N ENA (PWM)GPIO 33Remove ENA jumper first
L298N ENB (PWM)GPIO 19Remove ENB jumper first
OLED SDAGPIO 21I2C default
OLED SCLGPIO 22I2C default
Calibration buttonGPIO 0Internal pull-up; active LOW
Arduino Code
esp32-line-following-robot_intermediate.ino
// ESP32 Line Following Robot - Intermediate (5-sensor PID)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>

#define N 5
const int S[N] = {36,39,34,35,32};
const int IN1=25,IN2=26,IN3=27,IN4=14;
const int ENA_CH=0, ENB_CH=1, ENA_PIN=33, ENB_PIN=19;

float Kp=25.0, Ki=0.0, Kd=15.0;
const int BASE=180;
float lastErr=0, integral=0;
int thr[N];

Preferences prefs;
Adafruit_SSD1306 oled(128,64,&Wire,-1);

void setupPWM(){
  ledcSetup(ENA_CH,5000,8); ledcAttachPin(ENA_PIN,ENA_CH);
  ledcSetup(ENB_CH,5000,8); ledcAttachPin(ENB_PIN,ENB_CH);
}

void drive(int l, int r){
  l=constrain(l,-255,255); r=constrain(r,-255,255);
  if(l>=0){digitalWrite(IN1,HIGH);digitalWrite(IN2,LOW);}
  else    {digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH);l=-l;}
  if(r>=0){digitalWrite(IN3,HIGH);digitalWrite(IN4,LOW);}
  else    {digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH);r=-r;}
  ledcWrite(ENA_CH,l); ledcWrite(ENB_CH,r);
}

void calibrate(){
  int mn[N],mx[N];
  for(int i=0;i<N;i++){mn[i]=4095;mx[i]=0;}
  drive(150,-150);
  unsigned long t=millis();
  while(millis()-t<3000){
    for(int i=0;i<N;i++){
      int v=analogRead(S[i]);
      if(v<mn[i])mn[i]=v;
      if(v>mx[i])mx[i]=v;
    }
  }
  drive(0,0);
  prefs.begin("lfr",false);
  for(int i=0;i<N;i++){
    thr[i]=(mn[i]+mx[i])/2;
    prefs.putInt(String(i).c_str(),thr[i]);
  }
  prefs.end();
}

float position(){
  int sum=0,wsum=0;
  for(int i=0;i<N;i++){
    int on=(analogRead(S[i])>thr[i])?1:0;
    wsum+=on*i*1000; sum+=on;
  }
  if(sum==0) return lastErr>0?4000:0;
  return (float)wsum/sum;
}

void setup(){
  Serial.begin(115200);
  int pins[]={IN1,IN2,IN3,IN4};
  for(int p:pins) pinMode(p,OUTPUT);
  setupPWM();
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextSize(1); oled.setTextColor(WHITE);
  prefs.begin("lfr",true);
  for(int i=0;i<N;i++) thr[i]=prefs.getInt(String(i).c_str(),2048);
  prefs.end();
  pinMode(0,INPUT_PULLUP);
  if(digitalRead(0)==LOW) calibrate();
}

void loop(){
  float pos=position();
  float err=pos-2000.0f;
  integral+=err;
  float corr=Kp*err+Ki*integral+Kd*(err-lastErr);
  lastErr=err;
  drive(BASE+(int)corr, BASE-(int)corr);
  delay(5);
}
How It Works
01

Weighted-Average Position: Each sensor returns 1 if its analog reading exceeds the calibrated threshold (line detected). A weighted sum of active indices divided by total active sensors gives a position from 0 (far left) to 4000 (far right), with 2000 being centre.

02

PID Correction: Error = position - 2000. The proportional term gives immediate correction, the derivative term damps overshoot, and the integral term removes steady-state offset. Tune Kp first with Ki and Kd at zero.

03

LEDC PWM Speed: ledcSetup() configures two 8-bit 5 kHz PWM channels on ENA and ENB pins. ledcWrite() adjusts duty cycle 0-255 so the robot slows automatically on curves where PID correction is large.

04

NVS Calibration Storage: A 3-second calibration spin sweeps all sensors over both white and black surfaces. Min and max analog values are recorded per sensor and midpoint thresholds are stored in NVS flash using the Preferences library, surviving power cycles.

Applications
  • Competitive line-following robot events
  • Automated guided vehicle prototyping in maker labs
  • Teaching PID control theory to engineering students
  • Warehouse cart following floor tape paths
Troubleshooting

PID causes violent oscillation

Set Ki and Kd to zero and reduce Kp to 5. Increase Kp slowly until the robot tracks, then add Kd in small increments to reduce overshoot.

Calibration does not capture full range

Make sure the robot is positioned at the edge of the line before pressing the calibration button. Increase the spin time from 3 s to 5 s if sensors do not fully sweep.

Robot loses line on sharp turns at high speed

Reduce BASE speed constant from 180 to 120. The outer wheel needs more time to complete tight turns at lower speeds.

Upgrades
  • Add rotary encoders on both wheels for closed-loop speed matching
  • Display lap time on the OLED and save best lap to NVS
  • Add BLE serial to tune Kp, Ki, Kd from a smartphone without reflashing
  • Log position and correction data to an SD card for offline PID analysis
FAQ

You need an ESP32 DevKit, TODO: sensor, L298N IN1, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Robotics. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced build adds dual-mode operation: the PID line-follower can be overridden in real time by a smartphone via BLE UART. Wheel encoders provide closed-loop speed matching so both motors run at equal velocity regardless of battery voltage sag. A left-hand rule state machine allows the robot to solve simple mazes at junctions automatically, while live PID gains can be updated over BLE without reflashing the firmware.

Components
  • 1× ESP32 DevKit V1 — Built-in BLE 4.2
  • 2× Hall-effect wheel encoder disc + sensor — 20-slot disc per wheel
  • 1× 5-element IR sensor array — Same as intermediate
  • 1× L298N motor driver
  • 2× DC gear motor 12 V with encoder shaft — 150 RPM
  • 1× 11.1 V 3S Li-Po 1500 mAh — Matches 12 V motors
  • 1× SSD1306 OLED 128x64 — Shows mode and speed
Wiring
Component PinESP32 PinNotes
Left encoder signalGPIO 18Interrupt-capable; 3.3 V logic
Right encoder signalGPIO 23Interrupt-capable
Sensor array + motorsSame as intermediate
Mode status LEDGPIO 2ON = BLE manual mode
Arduino Code
esp32-line-following-robot_advanced.ino
// ESP32 Line Following Robot - Advanced (BLE + encoders + maze solver)
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>

#define SVC_UUID  "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define RX_UUID   "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define TX_UUID   "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

volatile long encL=0, encR=0;
void IRAM_ATTR iL(){ encL++; }
void IRAM_ATTR iR(){ encR++; }

bool autoMode=true;
String cmd="";
BLECharacteristic *pTx;

class RxCB: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *c) override {
    cmd = c->getValue().c_str();
  }
};

// PID and motor functions from intermediate level included here
// ...

void setup(){
  Serial.begin(115200);
  attachInterrupt(18,iL,RISING);
  attachInterrupt(23,iR,RISING);

  BLEDevice::init("ESP32-LineBot");
  BLEServer *srv = BLEDevice::createServer();
  BLEService *svc = srv->createService(SVC_UUID);
  pTx = svc->createCharacteristic(TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
  pTx->addDescriptor(new BLE2902());
  BLECharacteristic *rx = svc->createCharacteristic(RX_UUID, BLECharacteristic::PROPERTY_WRITE);
  rx->setCallbacks(new RxCB());
  svc->start();
  BLEDevice::getAdvertising()->start();
}

void loop(){
  if(cmd.length()){
    if(cmd=="A") autoMode=true;
    else if(cmd=="M") autoMode=false;
    // F,B,L,R,S handled in manual drive function
    cmd="";
  }
  if(autoMode) runPID();
  else         runManual();

  // Encoder speed balancing: compare encL vs encR
  // and offset left/right PWM to match wheel speeds
  encL=0; encR=0;
  delay(5);
}
How It Works
01

BLE UART Service: The Nordic UART Service (NUS) UUID pair over BLE 4.2 lets any BLE terminal app (Serial Bluetooth Terminal on Android, LightBlue on iOS) send single-character commands. Compound commands like "P:30:0:20" update Kp, Ki, Kd at runtime without reflashing.

02

Encoder Interrupt Counting: Hall-effect sensors trigger IRAM-resident ISRs on each rising edge. The main loop reads and resets counters every 5 ms, comparing left vs right pulse counts to detect and correct speed mismatch caused by motor tolerance.

03

Dual-Mode Switching: The autoMode flag selects between PID line-following and BLE manual control. Switching from manual back to auto resets the PID integral accumulator to zero to prevent windup that accumulated during manual driving.

04

Left-Hand Maze Rule: At a junction where multiple sensors detect the line, the robot prioritises left turn, then straight, then right turn, then reverse. This deterministic rule guarantees escape from any simply-connected maze in finite steps.

Applications
  • RoboCup Junior Rescue Line competition entries
  • Research platform for navigation algorithm testing
  • Bluetooth-controlled demonstration at maker fairs
  • University-level control systems lab project
Troubleshooting

BLE drops connection when motors start

Add 100 nF ceramic capacitors across motor terminals and a 10 uF electrolytic on the 3.3 V rail. Ferrite beads on motor leads reduce high-frequency EMI that disrupts the BLE radio.

Encoder ISR misses pulses at high RPM

Move the PID computation to a hardware timer ISR running at 200 Hz so loop() latency does not cause missed encoder edges at speed.

Maze solver enters an infinite loop

The left-hand rule only works for simply-connected mazes. Add a visited-junction map using encoder odometry coordinates to detect revisits and break out.

Upgrades
  • Replace BLE with ESP-NOW for latency-free multi-robot swarm control
  • Add a camera module for visual line detection using colour blob tracking
  • Implement dead-reckoning SLAM using encoder odometry and an MPU6050 IMU
  • Add a colour sensor to read intersection markers for dynamic route selection
FAQ

You need an ESP32 DevKit, TODO: sensor, L298N IN1, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Robotics. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.