Obstacle Avoider¶
This is where your robot comes alive. Instead of following a script, it will sense its surroundings and decide what to do by itself: drive forwards, and when something gets in the way, back up and find another way around. Set it loose in a room and watch it explore.
You'll need:
- Your robot, with the ultrasonic sensor at the front
- 1 × push button (Button A) to start and stop it
- Optional: the piezo buzzer, for a "beep!" when it spots something
-
robot.py,button.pyanddistance.pysaved on the ESP32
Sense, think, act¶
Almost every robot, from a robot vacuum to a self-driving car, runs the same loop, over and over:
- Sense: read the sensors. How far away is the nearest thing in front of me?
- Think: decide what to do. Is that too close?
- Act: drive the motors. Keep going, or turn away.
Our obstacle avoider does exactly this, dozens of times a second.
Wiring¶
On top of the wiring from Build the Robot, add:
| Part | Pin | Connects to |
|---|---|---|
| Button A | one leg | GPIO 14, other leg to GND |
| Buzzer (optional) | + | GPIO 26, other leg to GND |
The ultrasonic sensor is already wired to GPIO 5 (TRIG) and GPIO 39 (ECHO). If you haven't used it before, the Ultrasonic Sensors lesson explains how it works.
Plan it out¶
- Set up the robot, the button, the distance sensor and the buzzer.
- Break it into functions:
chirp()beeps the buzzer.avoid()does the escape move: stop, beep, reverse a little, then spin left or right (picked at random) for a random amount of time.
- Main loop:
- If Button A was pressed, switch between running and stopped.
- If running, measure the distance. Too close?
avoid(). Otherwise, drive forwards.
Why a start button?
If the robot started driving the moment it powered up, it would shoot off the desk while you were still plugging in the power bank. The button means it waits until you're ready.
Code¶
Save this to the ESP32 as main.py.
# Obstacle Avoider: drive around on your own, dodging anything in the way.
# Press Button A to start and stop.
import random
import time
from machine import Pin, PWM
from robot import Robot
from button import Button
from distance import DistanceSensor
BUTTON_PIN = 14
BUZZER_PIN = 26
CRUISE_SPEED = 55 # percent
TOO_CLOSE_CM = 20 # react to anything nearer than this
robot = Robot()
button = Button(BUTTON_PIN)
sensor = DistanceSensor(trigger_pin=5, echo_pin=39)
buzzer = PWM(Pin(BUZZER_PIN), freq=2000, duty_u16=0)
def chirp():
buzzer.freq(2000)
buzzer.duty_u16(32768)
time.sleep_ms(60)
buzzer.duty_u16(0)
def avoid():
# stop, back away, then turn a random direction for a random time
robot.stop()
chirp()
robot.backward(50)
time.sleep_ms(400)
turn_ms = random.randint(300, 700)
if random.randint(0, 1) == 0:
robot.spin_left(55)
else:
robot.spin_right(55)
time.sleep_ms(turn_ms)
robot.stop()
running = False
print("Press Button A to start")
while True:
if button.was_pressed():
running = not running
print("Go!" if running else "Stopped")
if not running:
robot.stop()
if not running:
time.sleep_ms(10)
continue
distance = sensor.distance_cm() # None means nothing in range
if distance is not None and distance < TOO_CLOSE_CM:
print("Obstacle at", distance, "cm")
avoid()
else:
robot.forward(CRUISE_SPEED)
How it works¶
"Nothing there" is not zero¶
sensor.distance_cm() returns None when no echo came back. That happens
when nothing is within range, or the sound bounced off at an angle and never
returned. So the check is:
Checking for None first matters, because comparing None < 20 would
crash the program.
Why turn randomly?¶
If the robot always turned right, it could get stuck forever in a corner,
turning right into the other wall, then right again... Picking a random
direction and a random turn time (random.randint(300, 700) milliseconds)
means it will eventually wriggle its way out of almost anything. A lot of
simple robot vacuums use exactly this trick!
Tuning¶
CRUISE_SPEED: faster is more fun, but the robot needs time to stop. If it bumps into things before reacting, slow it down or raiseTOO_CLOSE_CM.TOO_CLOSE_CM: 20 cm works for most rooms. Tight spaces might need less.
Blind spots
The ultrasonic sensor "sees" a cone about 30° wide straight ahead. It can miss thin things like chair legs, soft things like cushions and curtains (they absorb the sound), and walls it approaches at a steep angle (the echo bounces away). That's why real robots use several different kinds of sensor.
Hold the button
While the robot is doing its escape move, the program isn't checking the button. If a quick tap doesn't stop it, hold the button down for a second.
Level up: a scanning head¶
Right now the robot turns a random way when blocked. A smarter robot would look around first, and turn towards whichever side is more open.
Mount the ultrasonic sensor on top of an SG90 servo (a small bracket, double-sided tape or even a folded piece of card works), facing forwards when the servo is at 90°. Connect the servo as in the Servo Motors lesson: signal to GPIO 13, power to VIN, ground to GND. If you've built the Sonar Radar, it's the same scanner, now on wheels!
# Obstacle Avoider, level 2: when blocked, look left and right with a
# servo-mounted sensor and turn towards the more open side.
# Press Button A to start and stop.
import time
from machine import Pin, PWM
from robot import Robot
from button import Button
from distance import DistanceSensor
BUTTON_PIN = 14
SERVO_PIN = 13
CRUISE_SPEED = 55
TOO_CLOSE_CM = 25
FAR_AWAY_CM = 250 # what "nothing in range" counts as
SERVO_MIN_NS = 500_000
SERVO_MAX_NS = 2_500_000
LOOK_AHEAD, LOOK_LEFT, LOOK_RIGHT = 90, 160, 20 # servo angles
TURN_MS_90 = 450
robot = Robot()
button = Button(BUTTON_PIN)
sensor = DistanceSensor(trigger_pin=5, echo_pin=39)
servo = PWM(Pin(SERVO_PIN), freq=50, duty_u16=0)
def set_angle(angle):
angle = max(0, min(180, angle))
servo.duty_ns(SERVO_MIN_NS + (SERVO_MAX_NS - SERVO_MIN_NS) * angle // 180)
def look(angle):
# point the sensor, give the servo time to get there, then measure
set_angle(angle)
time.sleep_ms(350)
distance = sensor.distance_cm()
if distance is None:
return FAR_AWAY_CM
return distance
def choose_new_direction():
robot.stop()
robot.backward(45)
time.sleep_ms(250)
robot.stop()
left = look(LOOK_LEFT)
right = look(LOOK_RIGHT)
look(LOOK_AHEAD)
print("Left:", left, "cm Right:", right, "cm")
if left < TOO_CLOSE_CM and right < TOO_CLOSE_CM:
robot.spin_right(55) # boxed in: turn around
time.sleep_ms(TURN_MS_90 * 2)
elif left > right:
robot.spin_left(55)
time.sleep_ms(TURN_MS_90)
else:
robot.spin_right(55)
time.sleep_ms(TURN_MS_90)
robot.stop()
set_angle(LOOK_AHEAD)
running = False
print("Press Button A to start")
while True:
if button.was_pressed():
running = not running
print("Go!" if running else "Stopped")
if not running:
robot.stop()
if not running:
time.sleep_ms(10)
continue
distance = sensor.distance_cm()
if distance is not None and distance < TOO_CLOSE_CM:
choose_new_direction()
else:
robot.forward(CRUISE_SPEED)
When something is in the way, choose_new_direction() backs up a little,
turns the servo to look left, then right, and measures the distance each
way with look(). The robot then spins towards the more open side. If both
sides are blocked, it turns right around.
Servo power
The servo is powered from the ESP32's VIN pin, which gets 5 V from the USB power bank. If the ESP32 restarts when the servo moves, your power bank may not be able to keep up. Try another one.
Make it your own¶
- Speed up when it's clear: drive faster when the nearest obstacle is far away, and slow down smoothly as things get closer. (Hint: work out the speed from the distance.)
- Headlights: add the NeoPixels or an RGB LED that glow green when the way is clear and red when it's reacting.
- Explorer mode: count how many obstacles it avoids and print the total when you stop it.
- Edge detector: point a line sensor down at the edge of a table and make the robot back away from drops. (Test this with a hand ready to catch it!)
Next up: Line Follower.