Community project

Autonomous Waste-Sorting Robot

ESP32
Photo of Autonomous Waste-Sorting Robot
Generated with AI

Aaroh

Published September 11, 2026

This autonomous waste-sorting robot uses line-following and object detection to autonomously navigate an arena and sort waste into three bins based on material type. The robot combines an ESP32 microcontroller with infrared line tracking, ultrasonic distance sensing, capacitive moisture detection, and inductive metal sensing to identify and route different waste materials to the correct disposal bin via servo-controlled gates.

This guide provides a complete wiring diagram, detailed parts list, assembly steps for the rolling frame and sensor array, and fully commented firmware. Builders will learn how to integrate multiple sensor types, coordinate servo control through a PWM driver, and implement state-machine logic for autonomous sorting behavior. The project is designed for robotics competitions and educational maker spaces.

Wiring diagram

Wiring diagram for Autonomous Waste-Sorting Robot

Gather all the parts

QtyComponent
1

5-channel 3.3V digital IR line-tracker array

3.3 V digital output module

Five small infrared sensors that see the black track and guide the robot along it.

1

HC-SR04P Ultrasonic Distance Sensor

3.3 V

3.3V-compatible HC-SR04P / SR04P ultrasonic distance sensor. Use VCC/GND/TRIG/ECHO directly with ESP32 3V3 logic; unlike 5V-only HC-SR04 modules, this preserves the parking sensor guide contract.

1

SSD1306 OLED

0.96 inch, I2C

0.96 inch 128x64 OLED display with I2C interface

1

DRV8833 Dual DC Motor Driver

dual H-bridge

Dual H-bridge DC/stepper motor driver IC or carrier module. VM motor supply range is 2.7V-10.8V; logic inputs support 1.8V, 3.3V, and 5V controllers. TI rates the PWP/RTY package options at 1.5A RMS and 2A peak per H-bridge at VM=5V and 25C; carrier-board current depends on board layout and cooling. Controlled with AIN1/AIN2 and BIN1/BIN2 inputs, which may be driven as digital or PWM signals. nSLEEP must be high for the bare IC, while some carrier boards pull it high by default. nFAULT is an open-drain fault output.

1

N20-6V-300RPM Micro Metal Gear Motor

6 V, 300 RPM

N20 form-factor brushed DC micro metal gear motor rated 6V with about 300 RPM no-load speed. Two bare motor terminals connect directly to a motor driver output channel such as DRV8833 AOUT1/AOUT2 or BOUT1/BOUT2. Direction and speed are controlled by the motor driver. No firmware library is required for the motor itself.

1

N20-6V-300RPM Micro Metal Gear Motor

6 V, 300 RPM

N20 form-factor brushed DC micro metal gear motor rated 6V with about 300 RPM no-load speed. Two bare motor terminals connect directly to a motor driver output channel such as DRV8833 AOUT1/AOUT2 or BOUT1/BOUT2. Direction and speed are controlled by the motor driver. No firmware library is required for the motor itself.

1

SG90 Servo

green-bin tilt

Micro servo motor (SG90)

1

SG90 Servo

blue-bin tilt

Micro servo motor (SG90)

1

SG90 Servo

black-bin tilt

Micro servo motor (SG90)

1

Gravity: IP65 Capacitive Soil Moisture Sensor

3.3 V output

Capacitive soil moisture sensor with IP65 waterproof and corrosion-resistant construction. Compatible with Arduino, ESP32, and Raspberry Pi.

1

3.3V digital inductive metal detector module

3.3 V digital output

A proximity module that signals when a metal can reaches the sorting chute.

1

MP1584 Buck Converter

set to 5 V

Adjustable buck (step-down) DC-DC converter, 4.5-28 V in -> 0.8-20 V out, ~3 A. Configured to 5 V to step a 9 V / 12 V supply or battery pack down to the board's 5 V rail.

1

Protected 3S Li-ion battery pack with BMS

11.1 V nominal, 2200 mAh

A protected 11.1V rechargeable battery pack that powers the mobile robot away from a USB cable.

1

PCA9685 PWM Servo Driver

16-channel I2C servo controller

NXP PCA9685-based 16-channel, 12-bit PWM/servo driver module. Controlled over I2C at default address 0x40, with address pins for additional addresses. Outputs 16 independent PWM channels at one shared configurable frequency; servos normally use about 50-60 Hz. VCC is the logic supply and sets the I2C/PWM logic level. V+ is the separate servo/load rail, commonly 5V-6V for servos. OE is active high disable and is pulled low by default on Adafruit-style breakouts.

Assemble it in 7 steps

1. Build the lower rolling frame

Bolt left_motor_1 and right_motor_1 to the lower plate with the wheels pointing straight ahead. Mount motor_driver_1 close to the motors. Connect left_motor_1 M+ to motor_driver_1 AOUT1 and M- to AOUT2 (left-wheel power); connect right_motor_1 M+ to BOUT1 and M- to BOUT2 (right-wheel power).

  • Twist each pair of motor wires together and keep them short; this helps stop motor noise reaching the sensors.
  • Never connect a motor wire to an ESP32 pin — motor power can damage the board.

2. Make the 5 V power rail

With battery_3s_1 disconnected, adjust buck_5v_1 with a multimeter until VOUT to GND measures exactly 5.0 V. Connect BAT+ to buck_5v_1 VIN and BAT- to buck_5v_1 GND (battery power). Connect VOUT to the 5 V rail and GND to the shared ground rail (regulated power). Feed the 5 V rail to the ESP32 VIN pin, motor_driver_1 VM, pca9685_1 V+, and the red VCC wire of every servo; connect all black or brown servo wires to the shared ground rail.

  • Use a screw-terminal power splitter and thicker wire for the battery, converter, motors, and servo power wires.
  • Set the converter to exactly 5.0 V before connecting the ESP32 — excessive voltage can permanently damage it.
  • Every part must share the same ground rail or its signal wire will not work reliably.

3. Wire the ESP32, motors, and distance sensor

Mount the ESP32 in the middle tier. Connect motor_driver_1 AIN1 to GPIO16, AIN2 to GPIO17, BIN1 to GPIO18, and BIN2 to GPIO19 (motor-control signals). Connect motor_driver_1 nSLEEP to 3V3 (keeps the motor driver awake). Connect ultrasonic_1 TRIG to GPIO25 and ECHO to GPIO26 (distance signals). Connect moisture_sensor_1 AOUT to GPIO34 (moisture-reading signal). Connect the sonar and moisture sensor VCC pins to 3V3 and their GND pins to GND.

  • Mount the ultrasonic sensor with its two round faces pointing forward and clear of the acrylic.
  • Use the 3.3 V HC-SR04P in this design; a regular 5 V HC-SR04 can send an unsafe 5 V signal into the ESP32.

4. Connect the screen and servo controller

Connect oled_1 VCC to ESP32 3V3 (screen power), GND to GND (screen ground), SDA to GPIO21 (screen data), and SCL to GPIO22 (screen clock). On pca9685_1, connect VCC to ESP32 3V3 (safe signal-level power), GND to GND (ground), SDA to GPIO21 (shared data), SCL to GPIO22 (shared clock), and V+ to the separate 5 V rail (servo power). Mount the screen where its text is easy to read.

  • The OLED and PCA9685 use the same SDA and SCL wires; join each matching pair together rather than choosing separate GPIO pins.
  • Keep pca9685_1 VCC on 3.3 V and V+ on 5 V — connecting 5 V to VCC can send unsafe voltage into ESP32 data pins.
  • Make sure VCC and GND are not swapped — swapped power can damage the screen or controller.

5. Fit the floor and waste sensors

Fix line_tracker_1 under the front of the robot, with its five sensors facing down about 3 to 5 mm above the mat. Connect VCC to 3V3 (power), GND to GND (ground), S1 to GPIO4, S2 to GPIO13, S3 to GPIO14, S4 to GPIO27, and S5 to GPIO33 (line signals, left to right). Mount metal_sensor_1 beside the chute and connect VCC to 3V3 (power), GND to GND (ground), and OUT to GPIO32 (metal signal). Put the sensing end of moisture_sensor_1 where waste touches it without wetting its electronics board.

  • Use black tape under the line tracker while adjusting its tiny sensitivity controls.
  • Do not let wet material touch the ESP32, motor driver, or buck converter — it can create a short circuit.

6. Connect the three bin servos through the PCA9685

Plug the wet_servo_1 three-wire connector into pca9685_1 PWM0, dry_servo_1 into PWM1, and metal_servo_1 into PWM2. On each servo plug, brown or black goes to GND (ground), red goes to V+ (5 V power), and orange or yellow goes to the PWM signal row (control). Fasten each servo horn only after its bin is in the closed, level position.

  • Most PCA9685 boards label the three rows GND, V+, and signal. Follow those printed labels instead of relying only on the connector direction.
  • A jammed servo can overheat its wires or make the ESP32 restart; switch off power if a servo buzzes continuously.
  • Do not power the servo row from the ESP32 3V3 pin — it cannot supply enough current.

7. Test safely on the arena mat

Lift the wheels off the table for the first test. Confirm oled_1 shows text, place black tape under line_tracker_1, then bring an object in front of ultrasonic_1. Test each bin with a small dry item before using damp material. Finally place the robot on the track and confirm it turns toward the black line.

  • If the robot turns away from the line, reverse the matching motor’s two wires at motor_driver_1, or change the line-sensor logic noted in the firmware.
  • Keep fingers, loose clothing, and test items away from the wheels and bin linkages while the battery is connected.

Review all connections

1. Connections between "battery_3s_1" and "ESP32"

Functionbattery_3s_1ESP32
powerBAT+ → MP1584 Buck Converter VINEXT
groundBAT- → MP1584 Buck Converter GNDEXT

2. Connections between "buck_5v_1" and "ESP32"

Functionbuck_5v_1ESP32
powerVOUTVIN
groundGNDGND

3. Connections between "line_tracker_1" and "ESP32"

Functionline_tracker_1ESP32
powerVCC3V3
groundGNDGND
digitalS4GPIO 27
digitalS5GPIO 33
digitalS1GPIO 4
digitalS2GPIO 13
digitalS3GPIO 14

4. Connections between "ultrasonic_1" and "ESP32"

Functionultrasonic_1ESP32
powerVCC3V3
groundGNDGND
digitalTRIGGPIO 25
digitalECHOGPIO 26

5. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

6. Connections between "wet_servo_1" and "ESP32"

Functionwet_servo_1ESP32
powerVCCVIN
groundGNDGND

7. Connections between "dry_servo_1" and "ESP32"

Functiondry_servo_1ESP32
powerVCCVIN
groundGNDGND

8. Connections between "metal_servo_1" and "ESP32"

Functionmetal_servo_1ESP32
powerVCCVIN
groundGNDGND

9. Connections between "motor_driver_1" and "ESP32"

Functionmotor_driver_1ESP32
powerVMVIN
groundGNDGND
pwmAIN1GPIO 16
pwmAIN2GPIO 17
pwmBIN1GPIO 18
pwmBIN2GPIO 19
powernSLEEP3V3

10. Connections between "left_motor_1" and "ESP32"

Functionleft_motor_1ESP32
dataM+ → DRV8833 Dual DC Motor Driver AOUT1EXT
dataM- → DRV8833 Dual DC Motor Driver AOUT2EXT

11. Connections between "right_motor_1" and "ESP32"

Functionright_motor_1ESP32
dataM+ → DRV8833 Dual DC Motor Driver BOUT1EXT
dataM- → DRV8833 Dual DC Motor Driver BOUT2EXT

12. Connections between "moisture_sensor_1" and "ESP32"

Functionmoisture_sensor_1ESP32
powerVCC3V3
groundGNDGND
analogAOUTGPIO 34

13. Connections between "metal_sensor_1" and "ESP32"

Functionmetal_sensor_1ESP32
powerVCC3V3
groundGNDGND
digitalOUTGPIO 32

14. Connections between "pca9685_1" and "ESP32"

Functionpca9685_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
powerV+VIN
digitalPWM0 → SG90 Servo SIGNALEXT
digitalPWM1 → SG90 Servo SIGNALEXT
digitalPWM2 → SG90 Servo SIGNALEXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_PWMServoDriver.h>

enum RobotState { FOLLOWING, STOPPED, SORTING };


// Forward declarations
void setMotors(int leftForward, int leftReverse, int rightForward, int rightReverse);
void stopMotors();
void driveForward();
void turnLeft();
void turnRight();
float readDistanceCm();
void drawStatus(float distanceCm, int moisture, bool metalDetected);
void tipBin(uint8_t channel, const char *label);
void followLine();

constexpr int SONAR_TRIG_PIN = 25;
constexpr int SONAR_ECHO_PIN = 26;
constexpr int LEFT_IN1_PIN = 16;
constexpr int LEFT_IN2_PIN = 17;
constexpr int RIGHT_IN1_PIN = 18;
constexpr int RIGHT_IN2_PIN = 19;
constexpr int MOISTURE_PIN = 34;
constexpr int LINE_S1_PIN = 4;
constexpr int LINE_S2_PIN = 13;
constexpr int LINE_S3_PIN = 14;
constexpr int LINE_S4_PIN = 27;
constexpr int LINE_S5_PIN = 33;
constexpr int METAL_SENSOR_PIN = 32;
constexpr int I2C_SDA_PIN = 21;
constexpr int I2C_SCL_PIN = 22;

constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t PCA9685_ADDRESS = 0x40;
constexpr uint8_t WET_SERVO_CHANNEL = 0;
constexpr uint8_t DRY_SERVO_CHANNEL = 1;
constexpr uint8_t METAL_SERVO_CHANNEL = 2;
constexpr uint16_t SERVO_CLOSED_PULSE = 180;
constexpr uint16_t SERVO_TIP_PULSE = 430;
constexpr int MOTOR_SPEED = 185;
constexpr int TURN_SPEED = 160;
constexpr float STOP_DISTANCE_CM = 10.0f;
constexpr int WET_THRESHOLD = 2100;
constexpr unsigned long STATUS_PERIOD_MS = 250;
constexpr unsigned long SORT_COOLDOWN_MS = 3000;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_PWMServoDriver servoDriver(PCA9685_ADDRESS);
RobotState robotState = FOLLOWING;
String statusLine = "Starting";
unsigned long lastStatusUpdate = 0;
unsigned long lastSortTime = 0;

void setMotors(int leftForward, int leftReverse, int rightForward, int rightReverse) {
  ledcWrite(0, leftForward);
  ledcWrite(1, leftReverse);
  ledcWrite(2, rightForward);
  ledcWrite(3, rightReverse);
}

void stopMotors() { setMotors(0, 0, 0, 0); }
void driveForward() { setMotors(MOTOR_SPEED, 0, MOTOR_SPEED, 0); }
void turnLeft() { setMotors(0, TURN_SPEED, TURN_SPEED, 0); }
void turnRight() { setMotors(TURN_SPEED, 0, 0, TURN_SPEED); }

float readDistanceCm() {
  digitalWrite(SONAR_TRIG_PIN, LOW);
  delayMicroseconds(3);
  digitalWrite(SONAR_TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(SONAR_TRIG_PIN, LOW);
  unsigned long pulse = pulseIn(SONAR_ECHO_PIN, HIGH, 25000);
  return pulse == 0 ? 999.0f : pulse * 0.0343f / 2.0f;
}

void drawStatus(float distanceCm, int moisture, bool metalDetected) {
  if (millis() - lastStatusUpdate < STATUS_PERIOD_MS) return;
  lastStatusUpdate = millis();
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Swachh-Bot 360");
  display.print("Mode: "); display.println(statusLine);
  display.print("Range: ");
  if (distanceCm >= 999.0f) display.println("--");
  else { display.print(distanceCm, 0); display.println(" cm"); }
  display.print("Moisture: "); display.println(moisture);
  display.print("Metal: "); display.println(metalDetected ? "YES" : "NO");
  display.display();
}

void tipBin(uint8_t channel, const char *label) {
  robotState = SORTING;
  statusLine = label;
  stopMotors();
  servoDriver.setPWM(channel, 0, SERVO_TIP_PULSE);
  delay(1100);
  servoDriver.setPWM(channel, 0, SERVO_CLOSED_PULSE);
  delay(500);
  lastSortTime = millis();
  statusLine = "Following line";
  robotState = FOLLOWING;
}

void followLine() {
  // Most line modules output LOW on black. Remove ! if your module outputs HIGH on black.
  bool leftOuter = !digitalRead(LINE_S1_PIN);
  bool leftInner = !digitalRead(LINE_S2_PIN);
  bool centre = !digitalRead(LINE_S3_PIN);
  bool rightInner = !digitalRead(LINE_S4_PIN);
  bool rightOuter = !digitalRead(LINE_S5_PIN);
  if (centre || (leftInner && rightInner)) driveForward();
  else if (leftOuter || leftInner) turnLeft();
  else if (rightOuter || rightInner) turnRight();
  else { stopMotors(); statusLine = "Line lost"; }
}

void setup() {
  Serial.begin(115200);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  pinMode(SONAR_TRIG_PIN, OUTPUT);
  pinMode(SONAR_ECHO_PIN, INPUT);
  pinMode(LINE_S1_PIN, INPUT);
  pinMode(LINE_S2_PIN, INPUT);
  pinMode(LINE_S3_PIN, INPUT);
  pinMode(LINE_S4_PIN, INPUT);
  pinMode(LINE_S5_PIN, INPUT);
  pinMode(METAL_SENSOR_PIN, INPUT);

  ledcAttachChannel(LEFT_IN1_PIN, 20000, 8, 0);
  ledcAttachChannel(LEFT_IN2_PIN, 20000, 8, 1);
  ledcAttachChannel(RIGHT_IN1_PIN, 20000, 8, 2);
  ledcAttachChannel(RIGHT_IN2_PIN, 20000, 8, 3);
  stopMotors();

  servoDriver.begin();
  servoDriver.setOscillatorFrequency(27000000);
  servoDriver.setPWMFreq(50);
  servoDriver.setPWM(WET_SERVO_CHANNEL, 0, SERVO_CLOSED_PULSE);
  servoDriver.setPWM(DRY_SERVO_CHANNEL, 0, SERVO_CLOSED_PULSE);
  servoDriver.setPWM(METAL_SERVO_CHANNEL, 0, SERVO_CLOSED_PULSE);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) Serial.println("OLED not found");
  statusLine = "Following line";
}

void loop() {
  float distanceCm = readDistanceCm();
  int moisture = analogRead(MOISTURE_PIN);
  bool metalDetected = !digitalRead(METAL_SENSOR_PIN);

  if (robotState == FOLLOWING && distanceCm > STOP_DISTANCE_CM) {
    statusLine = "Following line";
    followLine();
  } else if (robotState == FOLLOWING) {
    stopMotors();
    statusLine = "Object detected";
    robotState = STOPPED;
  }

  if (robotState == STOPPED && millis() - lastSortTime > SORT_COOLDOWN_MS) {
    if (metalDetected) tipBin(METAL_SERVO_CHANNEL, "METAL CANS");
    else if (moisture > WET_THRESHOLD) tipBin(WET_SERVO_CHANNEL, "WET ORGANIC");
    else tipBin(DRY_SERVO_CHANNEL, "DRY RECYCLABLE");
  }

  drawStatus(distanceCm, moisture, metalDetected);
  delay(15);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik