A sensor-driven robot car that drives itself around obstacles — and takes IR remote commands when it'd rather be told where to go. Built on an Arduino Uno, an ultrasonic rangefinder, and a servo-mounted "head" that scans the room like it's actually looking for trouble.
Googly runs in two modes off the same loop. In manual mode, an IR remote sends hex codes straight to the motor functions — forward, back, left, right, stop. Flip to auto mode and the ultrasonic sensor takes over, sweeping the servo head to measure distance and steering around whatever it finds in front of it.
Elegoo Arduino Uno R3, paired with a custom sensor expansion shield that breaks out clean headers for every sensor and motor lead.
Dual DC gear motors through an L298N motor controller board, PWM speed control on ENA/ENB.
HC-SR04 ultrasonic sensor on a servo mount — measures distance and picks a clear direction.
A 3-element line tracking sensor module reads reflected IR light to follow a marked path or edge.
A Bluetooth module and IR receiver/remote control give two independent ways to drive Googly manually.
Arduino Uno loop checks for incoming remote/Bluetooth input first, falls back to autonomous avoidance and line following.
Right level for you? This page is aimed at the middle ground — not a first "blink an LED" project, and not a full research-grade robotics build either. If you want to learn real code and the correct parts for a robot with actual sensing and control, without diving into something overly complex, the parts list above and the Code Walkthrough section below are meant to be a complete, adaptable reference for exactly that.
The finished chassis — 4WD platform, servo-mounted ultrasonic sensor up front, motor driver and receiver wired in on top.
Serial monitor showed decoded values dropping randomly, especially right after a motor direction change. Turned out the motor driver's switching noise was interfering with the receiver on the same rail — moving the IR receiver's ground further from the motor leads and adding a small delay after irrecv.resume() cleaned up the signal.
The ultrasonic sensor was mounted level, so anything below its beam height went undetected. Angling the servo sweep slightly downward and averaging three getDistance() readings per position got rid of the false "clear path" calls.
Sharp turns rely on stopping one side's motor rather than reversing it, so Googly needs more room to pivot than the room usually gives it. Next revision: reverse the inner wheel during turns for a tighter radius.
OpenFull loop below: check for an IR command first, otherwise run the obstacle-avoidance sweep. Placeholder note: the IR hex codes in switch(results.value) below are stand-ins — replace them with the values your own remote prints to Serial.
#include <IRremote.h>
#include <Servo.h>
#define ENA 5
#define ENB 6
#define IN1 7
#define IN2 8
#define IN3 9
#define IN4 11
#define trigPin A5
#define echoPin A4
#define IR_RECEIVE_PIN 12
// Replace with the hex values your remote actually sends
#define IR_FORWARD 0xFF18E7
#define IR_BACKWARD 0xFF4AB5
#define IR_LEFT 0xFF10EF
#define IR_RIGHT 0xFF5AA5
#define IR_STOP 0xFF38C7
Servo myservo;
IRrecv irrecv(IR_RECEIVE_PIN);
decode_results results;
int motorSpeed = 200;
bool autoMode = true;
void setup() {
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT);
pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT);
myservo.attach(3);
myservo.write(90);
Serial.begin(9600);
irrecv.enableIRIn();
}
void loop() {
if (irrecv.decode(&results)) {
autoMode = false;
switch (results.value) {
case IR_FORWARD: forward(); break;
case IR_BACKWARD: backward(); break;
case IR_LEFT: turnLeft(); break;
case IR_RIGHT: turnRight(); break;
case IR_STOP: brake(); autoMode = true; break;
}
irrecv.resume();
delay(15); // settle time to avoid noise re-triggering the receiver
} else if (autoMode) {
automaticAvoidance();
}
}
void forward() {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
analogWrite(ENA, motorSpeed); analogWrite(ENB, motorSpeed);
}
void backward() {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
analogWrite(ENA, motorSpeed); analogWrite(ENB, motorSpeed);
}
void turnLeft() {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); // left side reverse
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW); // right side forward
analogWrite(ENA, motorSpeed); analogWrite(ENB, motorSpeed);
}
void turnRight() {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); // left side forward
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH); // right side reverse
analogWrite(ENA, motorSpeed); analogWrite(ENB, motorSpeed);
}
void brake() {
digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW); digitalWrite(IN4, LOW);
analogWrite(ENA, 0); analogWrite(ENB, 0);
}
int getDistance() {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long duration = pulseIn(echoPin, HIGH, 30000); // 30ms timeout
if (duration == 0) return 999; // no echo = treat as clear
return duration * 0.034 / 2; // convert to cm
}
void automaticAvoidance() {
int distanceAhead = getDistance();
if (distanceAhead > 25) {
forward();
return;
}
brake();
delay(150);
myservo.write(150); // look left
delay(400);
int distanceLeft = getDistance();
myservo.write(30); // look right
delay(600);
int distanceRight = getDistance();
myservo.write(90); // recenter
delay(300);
if (distanceLeft > distanceRight) {
turnLeft();
} else {
turnRight();
}
delay(400);
brake();
}
A quick tour through the logic above, function by function.
if (irrecv.decode(&results)) {
autoMode = false;
switch (results.value) {
case IR_FORWARD: forward(); break;
...
}
irrecv.resume();
}
Every pass through loop() checks the IR receiver first. If a code came in, Googly switches to manual mode and runs whatever motor function matches that button. irrecv.resume() resets the receiver so it's ready for the next signal. If nothing came in and Googly is still in auto mode, control falls through to automaticAvoidance().
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
analogWrite(ENA, motorSpeed);
analogWrite(ENB, motorSpeed);
The L298N takes two direction pins per side (IN1/IN2 for the left motors, IN3/IN4 for the right) plus a PWM enable pin (ENA/ENB) that sets speed. Setting each side's pair to opposite HIGH/LOW spins both sides the same direction — forward. Turning just flips one side's pair instead.
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long duration = pulseIn(echoPin, HIGH, 30000);
return duration * 0.034 / 2;
The HC-SR04 sends a 10-microsecond pulse out the trigger pin, which fires an ultrasonic burst. pulseIn() times how long the echo pin stays high while the sound wave travels out and bounces back. Multiplying by the speed of sound (in cm/µs) and dividing by two — since the sound makes a round trip — gives the distance in centimeters.
myservo.write(150); // look left
int distanceLeft = getDistance();
myservo.write(30); // look right
int distanceRight = getDistance();
if (distanceLeft > distanceRight) turnLeft();
else turnRight();
When something's within 25cm ahead, Googly stops, sweeps the servo-mounted sensor left then right, and compares the two readings. Whichever side has more open space wins — that's the direction it turns. It's a simple heuristic, but it's enough to route around most obstacles without needing a full map of the room.