Project 6.6: "Intelligent Obstacle Avoider"
What you’ll learn
- ✅ Multi‑sensor detection: Read ultrasonic distance and three IR obstacle sensors together for reliable awareness.
- ✅ Simple mapping: Build a tiny, grid‑like memory of “free” vs “blocked” zones while moving.
- ✅ Decisions to avoid: Pick safe turns or stops using clear priority rules (STOP → TURN → GO).
- ✅ Unfamiliar navigation: Probe ahead, sidestep, and keep heading with a minimal explore loop.
- ✅ Route optimization: Reduce loops with visited marks and a “prefer new paths” rule.
Blocks glossary (used in this project)
- Digital input (IR): Three infrared obstacle sensors return 1 (blocked) / 0 (clear).
- Ultrasonic trig/echo: Measure distance in cm with precise microsecond timing.
- Digital outputs (motor driver): L298N pins for forward, back, left, right, stop.
- State variables: store headings, visited cells, and last decision.
- Serial println: Short “SENS:…”, “MAP:…”, “DECIDE:…”, “NAV:…” lines for visibility.
What you need
| Part | How many? | Pin connection (suggested) |
|---|---|---|
| D1 R32 (ESP32) | 1 | USB cable (30 cm) |
| HC‑SR04 ultrasonic | 1 | TRIG → Pin 27, ECHO → Pin 25, VCC, GND |
| IR obstacle sensors | 3 | Left → Pin 32, Center → Pin 33, Right → Pin 35 |
| L298N motor driver + motors | 1 | Left IN1→18, IN2→19; Right IN3→5, IN4→23 |
| Power for motors (external) | 1 | Shared ground with R32 |
Notes
- Keep ultrasonic and IR sensor grounds tied to the R32 ground.
- Aim IR sensors at equal height; test their logic (some boards invert signals—adjust threshold logic as needed).
- Ultrasonic echo is 5V on some modules—use a safe level shifter or a module with 3.3V‑safe echo.
Before you start
- USB cable connected
- Shared ground verified
- Serial monitor open and shows:
print("Ready!") # Confirm serial is working so you can see messages
Microprojects 1–5
Microproject 6.6.1 – Obstacle detection with multiple sensors
Goal: Read three IR sensors and ultrasonic distance; print a unified snapshot.
Blocks used:
- Digital input: Pins 32, 33, 35 for IR.
- Trig/echo timing: Measure ultrasonic distance.
- Serial println: “SENS:IR_L/C/R”, “SENS:ULTRA_CM”.
MicroPython code:
import machine # Import machine for Pin and timing
import time # Import time for delays and microsecond timing
# IR sensors as digital inputs
irL = machine.Pin(32, machine.Pin.IN) # Left IR sensor on Pin 32
irC = machine.Pin(33, machine.Pin.IN) # Center IR sensor on Pin 33
irR = machine.Pin(35, machine.Pin.IN) # Right IR sensor on Pin 35
print("IR:READY L=32 C=33 R=35") # Confirm IR sensor pins
# Ultrasonic HC-SR04 pins
trig = machine.Pin(27, machine.Pin.OUT) # Trigger pin on 27
echo = machine.Pin(25, machine.Pin.IN) # Echo pin on 25
print("ULTRA:READY TRIG=27 ECHO=25") # Confirm ultrasonic pins
def ultra_cm(): # Measure distance in centimeters
trig.value(0) # Ensure trigger starts LOW
time.sleep_us(3) # Short settle time
trig.value(1) # Set trigger HIGH to start pulse
time.sleep_us(10) # Keep HIGH for 10 microseconds
trig.value(0) # Set trigger LOW to finish pulse
while echo.value() == 0: # Wait for echo to go HIGH (pulse start)
pass # Busy-wait until pulse starts
start = time.ticks_us() # Record start time in microseconds
while echo.value() == 1: # Wait while echo stays HIGH (pulse duration)
pass # Busy-wait until pulse ends
end = time.ticks_us() # Record end time in microseconds
dur = time.ticks_diff(end, start) # Compute pulse width in microseconds
cm = dur // 58 # Convert to centimeters (approx: us/58)
return cm # Return integer centimeters
cm = ultra_cm() # Take one ultrasonic measurement
snap = (irL.value(), irC.value(), irR.value()) # Read IR sensor states
print("SENS:IR_L/C/R", snap) # Print IR snapshot (1=blocked, 0=clear typical)
print("SENS:ULTRA_CM", cm) # Print ultrasonic distance in cm
Reflection: One glance shows “who” sees an obstacle and “how far” it is.
Challenge:
- Easy: Read ultrasonic 3 times and print the average.
- Harder: Add a timeout guard to ultra_cm so it returns 999 if no echo arrives.
Microproject 6.6.2 – Simple obstacle mapping
Goal: Maintain a tiny rolling map of “front status”: FREE, NEAR, BLOCK.
Blocks used:
- Thresholds: near_cm and block_cm.
- List buffer: store last 5 snapshots.
MicroPython code:
near_cm = 30 # Distance threshold for NEAR obstacles
block_cm = 15 # Distance threshold for BLOCK obstacles
map_buf = [] # Buffer to store recent front states
print("MAP:THRESH near", near_cm, "block", block_cm) # Print mapping thresholds
def classify_front(irC_val, cm): # Classify front status from center IR and distance
if (irC_val == 1) or (cm <= block_cm): # If center sees obstacle or very close
status = "BLOCK" # Strong block status
elif cm <= near_cm: # If object is near but not blocking
status = "NEAR" # Near status
else: # Otherwise clear
status = "FREE" # Free status
print("MAP:FRONT", status) # Print classification
return status # Return status
cm = ultra_cm() # Measure distance once
front = classify_front(irC.value(), cm) # Classify using center IR and distance
map_buf.append(front) # Store status in buffer
if len(map_buf) > 5: # If buffer exceeds 5 entries
map_buf.pop(0) # Remove oldest entry
print("MAP:BUF", map_buf) # Print buffer contents
Reflection: A small buffer smooths decisions—one bad read won’t cause panic.
Challenge:
- Easy: Print “MAP:STABLE” when last 3 entries match.
- Harder: Track left/right “SIDE:HIT” if IR_L or IR_R stay 1 for two reads.
Microproject 6.6.3 – Decision‑making to avoid
Goal: Choose STOP, TURN_LEFT, TURN_RIGHT, or GO based on IR and distance.
Blocks used:
- Priority rule: BLOCK → STOP; side hits → turn; else → go.
- Serial println: “DECIDE:…”.
MicroPython code:
def decide_action(irL_val, irC_val, irR_val, cm): # Decide what the robot should do
if (irC_val == 1) or (cm <= block_cm): # If front is blocked
act = "STOP" # Stop immediately
elif irL_val == 1 and irR_val == 0: # If left side blocked and right clear
act = "TURN_RIGHT" # Turn right to avoid left obstacle
elif irR_val == 1 and irL_val == 0: # If right side blocked and left clear
act = "TURN_LEFT" # Turn left to avoid right obstacle
elif cm <= near_cm: # If near but not blocked
act = "SLOW_GO" # Move slowly forward
else: # Otherwise clear
act = "GO" # Move forward normally
print("DECIDE:", act) # Print decision
return act # Return decision
cm = ultra_cm() # Measure distance
act = decide_action(irL.value(), irC.value(), irR.value(), cm) # Decide action
print("NEXT:", act) # Print next action for clarity
Reflection: Simple rules make consistent behavior—students can read and trust each branch.
Challenge:
- Easy: Add “TURN_LEFT_HARD/RIGHT_HARD” when side+near happen together.
- Harder: Add a cooldown (200 ms) after STOP to avoid rapid flip‑flop.
Microproject 6.6.4 – Navigating in unfamiliar environments
Goal: Execute decisions with safe motor pulses and probing behavior.
Blocks used:
- Motor helpers: forward, slow_forward, left, right, stop.
- Probe move: tiny forward after a turn to test clearance.
MicroPython code:
# Motor pins for L298N driver
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Right IN4
print("MOTORS:READY 18/19 5/23") # Confirm motor pins
def motors_stop(): # Stop both motors
L_IN1.value(0) # Left IN1 LOW
L_IN2.value(0) # Left IN2 LOW
R_IN3.value(0) # Right IN3 LOW
R_IN4.value(0) # Right IN4 LOW
print("MOVE:STOP") # Print stop
def motors_forward(pulse=0.20): # Move forward for a pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD", pulse) # Print forward pulse
time.sleep(pulse) # Run motors for pulse duration
motors_stop() # Stop after pulse
def motors_forward_slow(pulse=0.12): # Move forward slowly
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD_SLOW", pulse) # Print slow forward pulse
time.sleep(pulse) # Run motors for pulse duration
motors_stop() # Stop after pulse
def turn_left(pulse=0.16): # Turn left in place
L_IN1.value(0) # Left backward LOW
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:LEFT", pulse) # Print left turn pulse
time.sleep(pulse) # Run turn for pulse
motors_stop() # Stop after turn
def turn_right(pulse=0.16): # Turn right in place
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(0) # Right backward LOW
R_IN4.value(1) # Right backward HIGH
print("MOVE:RIGHT", pulse) # Print right turn pulse
time.sleep(pulse) # Run turn for pulse
motors_stop() # Stop after turn
def probe_forward(): # Small probe forward to test space
print("NAV:PROBE") # Print probe action
motors_forward(pulse=0.10) # Move a tiny forward pulse
act = decide_action(irL.value(), irC.value(), irR.value(), ultra_cm()) # Decide based on sensors
if act == "STOP": # If blocked
motors_stop() # Stop immediately
elif act == "TURN_LEFT": # If need to turn left
turn_left(0.18) # Turn left slightly stronger
probe_forward() # Probe forward after turn
elif act == "TURN_RIGHT": # If need to turn right
turn_right(0.18) # Turn right slightly stronger
probe_forward() # Probe forward after turn
elif act == "SLOW_GO": # If near but not blocked
motors_forward_slow(0.12) # Move forward slowly
else: # If clear
motors_forward(0.20) # Move forward normally
Reflection: Safe pulses and tiny probes make exploration steady; no wild swings or stalls.
Challenge:
- Easy: Add a second probe if the first shows NEAR but not BLOCK.
- Harder: Track last turn direction and alternate to avoid hugging one wall forever.
Microproject 6.6.5 – Route optimization
Goal: Reduce cycles with visited markers and a “prefer new paths” rule.
Blocks used:
- Visited set: Hash simple coordinates or steps.
- Preference: Avoid choosing the last 2 recently visited headings.
MicroPython code:
visited = set() # Create a set to store visited tags
recent = [] # Create a list to store recent decisions
print("ROUTE:INIT") # Print route system init
def mark(tag): # Mark a visited tag (e.g., TURN_LEFT at index)
visited.add(tag) # Add tag to visited set
recent.append(tag) # Append tag to recent list
if len(recent) > 2: # If recent list grows beyond 2
recent.pop(0) # Remove oldest entry
print("ROUTE:VISITED", list(visited)) # Print visited list
def prefer_new(options): # Choose an option not in recent if possible
for opt in options: # Iterate possible decisions
if opt not in recent: # If option not in recent list
print("ROUTE:CHOICE", opt) # Print chosen option
return opt # Return this option
print("ROUTE:CHOICE_FALLBACK", options[0]) # Print fallback choice
return options[0] # Fallback to first option
# Example of preference usage
cval = irC.value() # Read center IR
lval = irL.value() # Read left IR
rval = irR.value() # Read right IR
cm = ultra_cm() # Read ultrasonic distance
act = decide_action(lval, cval, rval, cm) # Decide original action
if act in ("TURN_LEFT", "TURN_RIGHT"): # If turning
act = prefer_new([act, "SLOW_GO"]) # Prefer new turn; fallback to slow go
mark(act) # Mark decision
else: # If not turning
mark(act) # Mark decision directly
print("NAV:OPTIMIZED", act) # Print optimized action
Reflection: A tiny “memory” avoids same‑turn loops and feels smarter to watch.
Challenge:
- Easy: Add “ROUTE:RESET” every 30 seconds to clear old memory.
- Harder: Count turns per minute and reduce pulse time if turns exceed a limit.
Main project – Intelligent obstacle avoider
Blocks steps (with glossary)
- Sensor fusion: IR side hits + ultrasonic front distance with thresholds and guards.
- Mapping buffer: FREE/NEAR/BLOCK snapshots for front status smoothing.
- Decision rules: STOP, TURN_LEFT/RIGHT, SLOW_GO, GO with cooldowns.
- Exploration loop: Safe pulses, probes, and alternation to prevent hugging.
- Optimization: Visited markers and recent preference to reduce cycles.
MicroPython code (mirroring blocks)
# Project 6.6 – Intelligent Obstacle Avoider
import machine # Import machine for Pin control
import time # Import time for microsecond timing and delays
# IR sensors
irL = machine.Pin(32, machine.Pin.IN) # Left IR sensor
irC = machine.Pin(33, machine.Pin.IN) # Center IR sensor
irR = machine.Pin(35, machine.Pin.IN) # Right IR sensor
print("INIT: IR L=32 C=33 R=35") # Confirm IR pins
# Ultrasonic pins
trig = machine.Pin(27, machine.Pin.OUT) # Ultrasonic trigger
echo = machine.Pin(25, machine.Pin.IN) # Ultrasonic echo
print("INIT: ULTRA TRIG=27 ECHO=25") # Confirm ultrasonic pins
def ultra_cm(): # Measure distance (cm) with guards
trig.value(0) # Ensure trigger LOW
time.sleep_us(3) # Short settle
trig.value(1) # Trigger HIGH
time.sleep_us(10) # 10 us pulse
trig.value(0) # Trigger LOW
t0 = time.ticks_us() # Start timeout timer
while echo.value() == 0: # Wait for rising edge
if time.ticks_diff(time.ticks_us(), t0) > 20000: # 20 ms timeout
return 999 # Return large value if no echo
start = time.ticks_us() # Record start
t1 = start # Initialize watchdog timestamp
while echo.value() == 1: # Wait while echo HIGH
if time.ticks_diff(time.ticks_us(), t1) > 30000: # 30 ms watchdog
break # Break if pulse too long
t1 = time.ticks_us() # Update watchdog time
end = time.ticks_us() # Record end
dur = time.ticks_diff(end, start) # Compute duration
cm = dur // 58 # Convert to cm
return cm # Return distance
# Motor pins
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Right IN4
print("INIT: MOTORS 18/19 5/23") # Confirm motor pins
def motors_stop(): # Stop both motors
L_IN1.value(0) # Left IN1 LOW
L_IN2.value(0) # Left IN2 LOW
R_IN3.value(0) # Right IN3 LOW
R_IN4.value(0) # Right IN4 LOW
print("MOVE:STOP") # Print stop
def motors_forward(pulse=0.20): # Forward pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD", pulse) # Print forward
time.sleep(pulse) # Run pulse
motors_stop() # Stop
def motors_forward_slow(pulse=0.12): # Slow forward pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD_SLOW", pulse) # Print slow forward
time.sleep(pulse) # Run pulse
motors_stop() # Stop
def turn_left(pulse=0.16): # Left turn pulse
L_IN1.value(0) # Left backward LOW
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:LEFT", pulse) # Print left turn
time.sleep(pulse) # Run pulse
motors_stop() # Stop
def turn_right(pulse=0.16): # Right turn pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(0) # Right backward LOW
R_IN4.value(1) # Right backward HIGH
print("MOVE:RIGHT", pulse) # Print right turn
time.sleep(pulse) # Run pulse
motors_stop() # Stop
def probe_forward(): # Tiny forward probe
print("NAV:PROBE") # Print probe label
motors_forward(pulse=0.10) # Small forward pulse
near_cm = 30 # Threshold for NEAR
block_cm = 15 # Threshold for BLOCK
print("THRESH: NEAR", near_cm, "BLOCK", block_cm) # Print thresholds
map_buf = [] # Buffer for front status snapshots
visited = set() # Set of visited decisions
recent = [] # Recent decisions list
last_turn = "" # Store last turn direction
cooldown_ms = 200 # Cooldown after STOP
last_stop_time = 0 # Timestamp of last STOP
def classify_front(irC_val, cm): # Classify front status
if (irC_val == 1) or (cm <= block_cm): # If center blocked or too close
status = "BLOCK" # Block status
elif cm <= near_cm: # If near but not block
status = "NEAR" # Near status
else: # Otherwise free
status = "FREE" # Free status
print("MAP:FRONT", status) # Print status
return status # Return status
def decide_action(irL_val, irC_val, irR_val, cm): # Decide next move
if (irC_val == 1) or (cm <= block_cm): # If front blocked
act = "STOP" # Choose stop
elif irL_val == 1 and irR_val == 0: # Left blocked only
act = "TURN_RIGHT" # Choose right turn
elif irR_val == 1 and irL_val == 0: # Right blocked only
act = "TURN_LEFT" # Choose left turn
elif cm <= near_cm: # Near obstacle
act = "SLOW_GO" # Choose slow go
else: # Clear path
act = "GO" # Choose normal go
print("DECIDE:", act) # Print decision
return act # Return decision
def mark(tag): # Mark visited and recent decision
visited.add(tag) # Add to visited
recent.append(tag) # Append to recent list
if len(recent) > 2: # Limit recent size
recent.pop(0) # Remove oldest
print("ROUTE:VISITED", list(visited)) # Print visited
def prefer_new(options): # Prefer options not recently used
for opt in options: # Iterate options
if opt not in recent: # If not in recent
print("ROUTE:CHOICE", opt) # Print choice
return opt # Return choice
print("ROUTE:FALLBACK", options[0]) # Print fallback
return options[0] # Return fallback
print("RUN: Intelligent avoider") # Announce start
while True: # Main navigation loop
cm = ultra_cm() # Measure distance
front = classify_front(irC.value(), cm) # Classify front status
map_buf.append(front) # Push to buffer
if len(map_buf) > 5: # Keep last 5 entries
map_buf.pop(0) # Drop oldest
print("MAP:BUF", map_buf) # Print buffer
act = decide_action(irL.value(), irC.value(), irR.value(), cm) # Decide next action
if act == "STOP": # If stop decided
motors_stop() # Stop immediately
last_stop_time = time.ticks_ms() # Record stop time
time.sleep(cooldown_ms / 1000.0) # Apply cooldown
mark("STOP") # Mark stop
# Alternate turn preference after stop
alt = "TURN_LEFT" if last_turn != "TURN_LEFT" else "TURN_RIGHT" # Choose opposite of last turn
act = alt # Set act to alternate turn
print("NAV:ALT_AFTER_STOP", act) # Print alternate plan
if act in ("TURN_LEFT", "TURN_RIGHT"): # If turning
choice = prefer_new([act, "SLOW_GO"]) # Prefer fresh turn
mark(choice) # Mark decision
if choice == "TURN_LEFT": # Execute left turn
turn_left(0.18) # Run left turn pulse
last_turn = "TURN_LEFT" # Save last turn
probe_forward() # Probe forward
elif choice == "TURN_RIGHT": # Execute right turn
turn_right(0.18) # Run right turn pulse
last_turn = "TURN_RIGHT" # Save last turn
probe_forward() # Probe forward
else: # If SLOW_GO fallback
motors_forward_slow(0.12) # Slow forward pulse
elif act == "SLOW_GO": # If slow go
mark("SLOW_GO") # Mark decision
motors_forward_slow(0.12) # Execute slow forward
elif act == "GO": # If normal go
mark("GO") # Mark decision
motors_forward(0.20) # Execute forward
time.sleep(0.05) # Small loop delay
External explanation
- What it teaches: You fused fast IR hits with ultrasonic distance, smoothed status with a buffer, made clean decisions, explored safely with pulses and probes, and added a simple memory to avoid loops.
- Why it works: IR gives instant “which side” info, ultrasonic gives “how far,” buffers reduce flukes, cooldowns prevent flip‑flops, and “prefer new” choices break cycles.
- Key concept: “Sense → buffer → decide → act → learn.”
Story time
Your robot noses forward, senses a wall, and pivots like it knows the room. It tries a different route, probes ahead, and glides through—no frantic spinning, just smart, steady choices.
Debugging (2)
Debugging 6.6.1 – Does not detect obstacles of a certain color/texture
Problem: IR sensors miss glossy black or matte white surfaces.
Clues: IR_L/C/R stay 0 while ultrasonic shows small cm.
Broken code:
# Trust IR center alone for BLOCK
if irC_val == 1:
status = "BLOCK"
Fixed code:
# Combine IR with ultrasonic threshold
if (irC_val == 1) or (cm <= block_cm): # Use distance as a backup
status = "BLOCK" # Strong block even if IR fails
Why it works: Ultrasonic distance doesn’t care about color; combining sensors catches tricky surfaces.
Avoid next time: Use more than one sensor type for critical stops.
Debugging 6.6.2 – Cycles in navigation
Problem: Robot keeps turning the same way and loops.
Clues: Recent decisions show repeated “TURN_RIGHT”.
Broken code:
act = "TURN_RIGHT" # Hard-coded preference
Fixed code:
act = prefer_new([act, "SLOW_GO"]) # Prefer not-recent actions
last_turn = "TURN_RIGHT" # Track last turn and alternate after STOP
Why it works: Preference and alternation break repetitive loops and encourage fresh paths.
Avoid next time: Track history and avoid repeating the last choice too often.
Final checklist
- IR and ultrasonic sensors print clear snapshots (IR_L/C/R and ULTRA_CM)
- Front status buffer stabilizes decisions (FREE/NEAR/BLOCK)
- Decisions choose STOP/TURN/SLOW_GO/GO with sensible thresholds
- Exploration loop uses safe pulses and probes to test space
- Route optimization reduces loops with visited and recent preferences
Extras
- 🧠 Student tip: Log “DECIDE” and “MAP:FRONT” while driving different rooms—thresholds get easy to tune with data.
- 🧑🏫 Instructor tip: Have teams draw a decision tree (STOP → TURN → PROBE → GO) before coding; it prevents chaotic movement.
- 📖 Glossary:
- Probe: A small, safe forward pulse to test if space is clear.
- Cooldown: Short pause after STOP to avoid flip‑flop decisions.
- Visited: Simple memory to avoid repeating the same turn again.
- 💡 Mini tips:
- Keep sensor heights matched; misalignment creates false side hits.
- Re‑test ultrasonic thresholds when battery voltage changes.
- Print short, labeled lines so classroom debugging stays readable.