Project 6.3: "Advanced Line Follower"
What you’ll learn
- ✅ Basic line tracking: Read three TCRT5000 sensors and follow a dark line on a light surface (or the inverse).
- ✅ Multi‑sensor logic: Decide turns using left, center, and right readings together.
- ✅ Handling curves & intersections: Slow down in tight curves and choose paths at splits.
- ✅ Adaptive speed: Change motor speed based on how “off center” you are.
- ✅ Recovery search: If the line is lost, run a safe search pattern to find it again fast.
Blocks glossary (used in this project)
- Analog input (ADC): Read each TCRT5000 reflectance sensor as 0–4095.
- Thresholding: Convert ADC values into boolean “ON_LINE / OFF_LINE.”
- Digital outputs (motor driver): Control L298N IN pins for left/right motors.
- Speed variable: Use simple duty duration or step timing to simulate speed scaling.
- Serial println: Print “L/C/R”, “STATE:…”, “TURN:…”, and “SPEED:…” for visibility.
- If / else + patterns: Choose forward/turn/slow/search behaviors from sensor states.
What you need
| Part | How many? | Pin connection (suggested) |
|---|---|---|
| D1 R32 | 1 | USB cable (30 cm) |
| TCRT5000 reflectance | 3 | Left → ADC Pin 32, Center → ADC Pin 33, Right → ADC Pin 34 |
| L298N motor driver | 1 | Left IN1→18, IN2→19; Right IN3→5, IN4→23 |
| TT motors + wheels | 2 | L298N OUT1/OUT2 (left), OUT3/OUT4 (right) |
| Power for motors | 1 | External motor supply; shared ground with R32 |
Notes
- Aim sensors downward a few mm from the floor.
- Share ground between the sensor boards, L298N, and the D1 R32.
- Calibrate thresholds for your floor/line colors.
Before you start
- USB cable is plugged in and stable
- Serial monitor is open
- Quick test prints:
print("Ready!") # Confirm serial is working so you can see messages
Microprojects 1–5
Microproject 6.3.1 – Basic line tracking
Goal: Read L/C/R sensors, threshold them, and choose forward/left/right.
Blocks used:
- ADC read: Pins 32, 33, 34.
- Thresholding: Make booleans for “line detected.”
- Motor control: Forward/turn with simple prints.
MicroPython code:
import machine # Import machine to access ADC and motor pins
import time # Import time for small delays
# Sensors: Left, Center, Right on ADC pins
adcL = machine.ADC(machine.Pin(32)) # Create ADC for Left sensor on Pin 32
adcC = machine.ADC(machine.Pin(33)) # Create ADC for Center sensor on Pin 33
adcR = machine.ADC(machine.Pin(34)) # Create ADC for Right sensor on Pin 34 (input-only pin)
adcL.atten(machine.ADC.ATTN_11DB) # Set attenuation for full range on Left
adcC.atten(machine.ADC.ATTN_11DB) # Set attenuation for full range on Center
adcR.atten(machine.ADC.ATTN_11DB) # Set attenuation for full range on Right
adcL.width(machine.ADC.WIDTH_12BIT) # Use 12-bit resolution (0–4095) for Left
adcC.width(machine.ADC.WIDTH_12BIT) # Use 12-bit resolution (0–4095) for Center
adcR.width(machine.ADC.WIDTH_12BIT) # Use 12-bit resolution (0–4095) for Right
print("Microproject 6.3.1: Sensors ready (L=32, C=33, R=34)") # Confirm sensor setup
# Motors: L298N IN pins
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Left motor IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Left motor IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Right motor IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Right motor IN4
print("Motors ready (Left 18/19, Right 5/23)") # Confirm motor setup
def motors_stop(): # Helper to 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("MOTORS:STOP") # Print stop status
def motors_forward(): # Helper to move forward
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") # Print forward status
def motors_left(): # Helper to turn left (pivot)
L_IN1.value(0) # Left backward LOW (stop/pivot)
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("TURN:LEFT") # Print left turn
def motors_right(): # Helper to turn right (pivot)
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("TURN:RIGHT") # Print right turn
threshold = 2000 # Initial reflectance threshold (tune for your surface)
print("Threshold:", threshold) # Show threshold value
# Read once and act (basic behavior demo)
L_raw = adcL.read() # Read Left sensor raw value
C_raw = adcC.read() # Read Center sensor raw value
R_raw = adcR.read() # Read Right sensor raw value
print("RAW L/C/R:", L_raw, C_raw, R_raw) # Print raw readings
L_on = L_raw > threshold # True if Left detects dark line (if higher=dark)
C_on = C_raw > threshold # True if Center detects dark line
R_on = R_raw > threshold # True if Right detects dark line
print("ON_LINE L/C/R:", L_on, C_on, R_on) # Print boolean detection states
if C_on and not L_on and not R_on: # If only Center sees the line
motors_forward() # Move forward to stay centered
elif L_on and not C_on: # If Left sees line and Center does not
motors_left() # Turn left to re-center
elif R_on and not C_on: # If Right sees line and Center does not
motors_right() # Turn right to re-center
else: # For other combinations (e.g., intersections or none)
motors_stop() # Stop as a safe default
Reflection: Thresholds turn analog light into clear “on line” decisions—center forward, side turn.
Challenge:
- Easy: Invert logic if your sensor gives lower values on dark lines.
- Harder: Add a tiny forward pulse before stopping to avoid jitter at edges.
Microproject 6.3.2 – Multi‑sensor tracking
Goal: Use all three sensors together to handle typical patterns like slight drift or dual hits.
Blocks used:
- Combined rules: Handle L+C and C+R patterns smoothly.
- Serial println: Print “RULE:LC/CR/ONLY_L/ONLY_R/CENTER”.
MicroPython code:
import time # Import time for small delays
def read_states(threshold): # Helper to read and threshold all sensors
L_raw = adcL.read() # Read Left ADC
C_raw = adcC.read() # Read Center ADC
R_raw = adcR.read() # Read Right ADC
L_on = L_raw > threshold # Threshold Left
C_on = C_raw > threshold # Threshold Center
R_on = R_raw > threshold # Threshold Right
print("RAW:", L_raw, C_raw, R_raw) # Print raw readings
print("STATE:", L_on, C_on, R_on) # Print boolean states
return L_on, C_on, R_on # Return booleans
def step_multi(threshold): # One decision step using multi-sensor rules
L_on, C_on, R_on = read_states(threshold) # Read states
if C_on and not L_on and not R_on: # Center only → go forward
print("RULE:CENTER") # Print rule
motors_forward() # Forward
elif L_on and C_on and not R_on: # Left+Center → gentle left bias forward
print("RULE:LC") # Print rule
motors_left() # Turn left
time.sleep(0.1) # Short correction pulse
motors_forward() # Then forward
elif R_on and C_on and not L_on: # Right+Center → gentle right bias forward
print("RULE:CR") # Print rule
motors_right() # Turn right
time.sleep(0.1) # Short correction pulse
motors_forward() # Then forward
elif L_on and not C_on and not R_on: # Only Left → stronger left correction
print("RULE:ONLY_L") # Print rule
motors_left() # Turn left
elif R_on and not C_on and not L_on: # Only Right → stronger right correction
print("RULE:ONLY_R") # Print rule
motors_right() # Turn right
else: # Ambiguous (all off or all on at an intersection)
print("RULE:AMBIG") # Print rule
motors_stop() # Stop to think
# Demo calls (two steps)
step_multi(threshold) # Run one multi-sensor decision
time.sleep(0.2) # Small delay
step_multi(threshold) # Run again
Reflection: Combining center with a side makes turns gentle—less zig‑zag, more stable motion.
Challenge:
- Easy: Increase correction pulse to 150 ms for stronger steering.
- Harder: Add a “both sides on, center off” rule to slow down before deciding.
Microproject 6.3.3 – Tight curves and intersections
Goal: Detect sharp curves (side sensor only for several reads) and intersections (all three on).
Blocks used:
- Counters/windows: Confirm a state over time.
- Speed scaling: Slow down on tight curves and at intersections.
MicroPython code:
curve_window = 5 # Number of consecutive steps to confirm a tight curve
left_hits = 0 # Counter for consecutive left-only detections
right_hits = 0 # Counter for consecutive right-only detections
def curve_intersection_step(threshold): # One step with curve/intersection logic
global left_hits # Use global counter for left-only
global right_hits # Use global counter for right-only
L_on, C_on, R_on = read_states(threshold) # Read sensor states
if L_on and not C_on and not R_on: # Left-only (possible tight curve)
left_hits += 1 # Increase left curve counter
right_hits = 0 # Reset right curve counter
print("CURVE_LEFT_HITS:", left_hits) # Print counter
motors_left() # Turn left
time.sleep(0.08) # Brief slow movement to handle curve
elif R_on and not C_on and not L_on: # Right-only (possible tight curve)
right_hits += 1 # Increase right curve counter
left_hits = 0 # Reset left curve counter
print("CURVE_RIGHT_HITS:", right_hits) # Print counter
motors_right() # Turn right
time.sleep(0.08) # Brief slow movement to handle curve
elif L_on and C_on and R_on: # All three on (intersection or wide line)
print("INTERSECTION:DETECTED") # Print intersection message
motors_stop() # Stop briefly to choose path
time.sleep(0.2) # Short pause
motors_forward() # Default choice: go straight through
time.sleep(0.2) # Move forward a little
left_hits = 0 # Reset curve counters
right_hits = 0 # Reset curve counters
else: # Regular tracking
left_hits = 0 # Reset left curve counter
right_hits = 0 # Reset right curve counter
step_multi(threshold) # Use normal multi-sensor rules
# Demo: run several steps
for i in range(10): # Run 10 decision steps
curve_intersection_step(threshold) # Handle curve/intersection logic
time.sleep(0.05) # Small delay between steps
Reflection: Windows and pauses make curves and intersections smooth instead of chaotic.
Challenge:
- Easy: At intersections, add “TURN:LEFT” choice sometimes (e.g., every other intersection).
- Harder: Use a “mode” variable (LEFT_BIAS/RIGHT_BIAS) to choose intersection behavior consistently.
Microproject 6.3.4 – Speed adjustment on curves
Goal: Adapt speed using a simple error value: side sensors increase error, center reduces it.
Blocks used:
- Error metric: error = (R_on − L_on)*k with center bonus.
- Speed mapping: Faster when centered, slower when off center.
MicroPython code:
def compute_error(L_on, C_on, R_on): # Compute a simple lateral error
base = 0 # Start error at zero
if L_on and not R_on: # If left sees line and right does not
base -= 1 # Bias left (negative)
if R_on and not L_on: # If right sees line and left does not
base += 1 # Bias right (positive)
if C_on: # If center sees line
base *= 0.5 # Reduce error when center is on line
print("ERROR:", base) # Print computed error
return base # Return error value
def set_speed(error): # Simulate speed by controlling move pulse length
if abs(error) < 0.5: # If error is small (well centered)
pulse = 0.12 # Longer forward pulse (faster)
print("SPEED:FAST") # Print speed label
else: # If error is larger (off center)
pulse = 0.06 # Shorter forward pulse (slower)
print("SPEED:SLOW") # Print speed label
return pulse # Return pulse duration
def speed_step(threshold): # One step that adapts speed
L_on, C_on, R_on = read_states(threshold) # Read states
err = compute_error(L_on, C_on, R_on) # Compute error
pulse = set_speed(err) # Choose pulse based on error
if C_on: # If center sees line
motors_forward() # Move forward
time.sleep(pulse) # Move for chosen pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Small pause
elif L_on and not R_on: # If only left sees the line
motors_left() # Turn left
time.sleep(pulse) # Turn for chosen pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Small pause
elif R_on and not L_on: # If only right sees the line
motors_right() # Turn right
time.sleep(pulse) # Turn for chosen pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Small pause
else: # If none see the line
motors_stop() # Stop for safety
print("STATE:LOST") # Print lost state
# Demo: run a few adaptive-speed steps
for i in range(8): # Run 8 adaptive steps
speed_step(threshold) # Perform speed-adjusted move
Reflection: Even a tiny speed change reduces overshoot—smooth movement feels smarter.
Challenge:
- Easy: Add “SPEED:MID” as a third level.
- Harder: Scale pulse by a small table mapping |error| to pulse times.
Microproject 6.3.5 – Searching for a lost line
Goal: If no sensors see the line, run a safe search pattern (wiggle left/right, then forward).
Blocks used:
- State check: Detect NONE_ON_LINE.
- Pattern: Left wiggle → Right wiggle → Short forward.
MicroPython code:
def lost_line(threshold): # Handle a lost line condition
L_on, C_on, R_on = read_states(threshold) # Read states
if not L_on and not C_on and not R_on: # If all sensors are off line
print("SEARCH:START") # Announce search start
motors_left() # Wiggle left
time.sleep(0.15) # Short left wiggle
motors_right() # Wiggle right
time.sleep(0.15) # Short right wiggle
motors_forward() # Small forward nudge
time.sleep(0.12) # Forward nudge duration
motors_stop() # Stop
print("SEARCH:END") # Announce search end
else: # If some sensor sees the line
print("SEARCH:SKIP") # Skip search
step_multi(threshold) # Continue normal tracking
# Demo: run lost-line handler a few times
for i in range(5): # Attempt search steps
lost_line(threshold) # Call lost-line handler
time.sleep(0.1) # Small delay
Reflection: A gentle wiggle saves time; big spinning wastes battery and can lose orientation.
Challenge:
- Easy: Add a second search pass with longer wiggles if the first fails.
- Harder: Remember the last seen side (LEFT or RIGHT) and bias the search toward it.
Main project – Advanced line follower
Blocks steps (with glossary)
- Sensor reads + thresholds: Convert ADC to clear booleans for L/C/R.
- Multi‑sensor rules: Forward when centered, corrective turns for side hits.
- Curves/intersections: Confirm with counters; slow down and decide.
- Adaptive speed: Map simple error to pulse length.
- Recovery: Run a safe search pattern when line is lost.
MicroPython code (mirroring blocks)
# Project 6.3 – Advanced Line Follower
import machine # Import machine for ADC and motor control
import time # Import time for delays and timing
# Sensors setup: ADC Left, Center, Right
adcL = machine.ADC(machine.Pin(32)) # Left sensor on Pin 32
adcC = machine.ADC(machine.Pin(33)) # Center sensor on Pin 33
adcR = machine.ADC(machine.Pin(34)) # Right sensor on Pin 34
for adc in (adcL, adcC, adcR): # Configure each ADC similarly
adc.atten(machine.ADC.ATTN_11DB) # Full-scale attenuation
adc.width(machine.ADC.WIDTH_12BIT) # 12-bit resolution (0–4095)
print("INIT: Sensors L=32, C=33, R=34") # Confirm sensor pins
# Motors setup: L298N IN 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 Left(18,19) Right(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("MOTORS:STOP") # Print stop
def motors_forward(): # Move forward
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") # Print forward
def motors_left(): # Pivot left
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("TURN:LEFT") # Print left turn
def motors_right(): # Pivot right
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("TURN:RIGHT") # Print right turn
threshold = 2000 # Reflectance threshold (tune for your surface)
print("THRESHOLD:", threshold) # Show threshold
curve_window = 5 # Steps to confirm tight curve
left_hits = 0 # Consecutive left-only counter
right_hits = 0 # Consecutive right-only counter
def read_states(th): # Read and threshold sensors
L_raw = adcL.read() # Read Left raw
C_raw = adcC.read() # Read Center raw
R_raw = adcR.read() # Read Right raw
L_on = L_raw > th # Threshold Left
C_on = C_raw > th # Threshold Center
R_on = R_raw > th # Threshold Right
print("RAW L/C/R:", L_raw, C_raw, R_raw) # Print raw
print("ON_LINE L/C/R:", L_on, C_on, R_on) # Print booleans
return L_on, C_on, R_on # Return states
def compute_error(L_on, C_on, R_on): # Simple lateral error
base = 0 # Start at zero
if L_on and not R_on: # Left bias
base -= 1 # Negative error
if R_on and not L_on: # Right bias
base += 1 # Positive error
if C_on: # Center helps stability
base *= 0.5 # Reduce error magnitude
print("ERROR:", base) # Print error
return base # Return error
def set_speed(error): # Map error to speed pulse
if abs(error) < 0.5: # Well centered
pulse = 0.12 # Faster pulse
print("SPEED:FAST") # Print speed
else: # Off center
pulse = 0.06 # Slower pulse
print("SPEED:SLOW") # Print speed
return pulse # Return pulse length
def step_multi(th): # Multi-sensor rule step
L_on, C_on, R_on = read_states(th) # Get states
if C_on and not L_on and not R_on: # Center only
print("RULE:CENTER") # Print rule
motors_forward() # Forward
elif L_on and C_on and not R_on: # Left+Center
print("RULE:LC") # Print rule
motors_left() # Turn left
time.sleep(0.1) # Correction pulse
motors_forward() # Forward
elif R_on and C_on and not L_on: # Right+Center
print("RULE:CR") # Print rule
motors_right() # Turn right
time.sleep(0.1) # Correction pulse
motors_forward() # Forward
elif L_on and not C_on and not R_on: # Only Left
print("RULE:ONLY_L") # Print rule
motors_left() # Turn left
elif R_on and not C_on and not L_on: # Only Right
print("RULE:ONLY_R") # Print rule
motors_right() # Turn right
else: # Ambiguous
print("RULE:AMBIG") # Print rule
motors_stop() # Stop
def curve_intersection_step(th): # Handle curves/intersections
global left_hits # Use global counters
global right_hits # Use global counters
L_on, C_on, R_on = read_states(th) # Read states
if L_on and not C_on and not R_on: # Left-only
left_hits += 1 # Count left curve
right_hits = 0 # Reset right
print("CURVE_LEFT_HITS:", left_hits) # Print counter
motors_left() # Turn left
time.sleep(0.08) # Slow turn
elif R_on and not C_on and not L_on: # Right-only
right_hits += 1 # Count right curve
left_hits = 0 # Reset left
print("CURVE_RIGHT_HITS:", right_hits) # Print counter
motors_right() # Turn right
time.sleep(0.08) # Slow turn
elif L_on and C_on and R_on: # Intersection
print("INTERSECTION") # Print intersection
motors_stop() # Pause
time.sleep(0.2) # Decide time
motors_forward() # Default straight
time.sleep(0.2) # Move a bit
left_hits = 0 # Reset counters
right_hits = 0 # Reset counters
else: # Regular tracking
step_multi(th) # Use multi-sensor rules
def speed_step(th): # Adaptive speed step
L_on, C_on, R_on = read_states(th) # Read states
err = compute_error(L_on, C_on, R_on) # Compute error
pulse = set_speed(err) # Speed mapping
if C_on: # Center on line
motors_forward() # Forward
time.sleep(pulse) # Move pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Pause
elif L_on and not R_on: # Left-only
motors_left() # Turn left
time.sleep(pulse) # Move pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Pause
elif R_on and not L_on: # Right-only
motors_right() # Turn right
time.sleep(pulse) # Move pulse
motors_stop() # Stop briefly
time.sleep(0.03) # Pause
else: # Lost
motors_stop() # Stop
print("STATE:LOST") # Print lost
def lost_line(th): # Recovery search
L_on, C_on, R_on = read_states(th) # Read states
if not L_on and not C_on and not R_on: # All off
print("SEARCH:START") # Announce search
motors_left() # Wiggle left
time.sleep(0.15) # Wiggle
motors_right() # Wiggle right
time.sleep(0.15) # Wiggle
motors_forward() # Nudge forward
time.sleep(0.12) # Nudge duration
motors_stop() # Stop
print("SEARCH:END") # Announce end
else: # Some see line
step_multi(th) # Normal tracking
print("RUN: Advanced line follower") # Announce start
while True: # Continuous control loop
curve_intersection_step(threshold) # Handle curves/intersections first
speed_step(threshold) # Adapt speed using error
lost_line(threshold) # Try recovery if needed
time.sleep(0.05) # Small loop delay
External explanation
- What it teaches: You turned analog reflectance into decisions, handled tricky corners and intersections, adjusted speed with a simple error, and added a recovery search.
- Why it works: Multi‑sensor rules reduce zig‑zagging, counters confirm curves, speed scaling reduces overshoot, and search patterns quickly re‑acquire the line.
- Key concept: “Sense → decide → adjust → recover.”
Story time
Your robot glides along the tape trail, slows for a tight bend, pauses at a crossroads, and then shoots straight with confidence. When it slips, it wiggles, finds the track, and keeps going—like it’s stubborn in a good way.
Debugging (2)
Debugging 6.3.1 – Loses the line on curves
Problem: Robot shoots past tight bends and ends up off the track.
Clues: Side‑only states appear briefly, robot keeps full speed.
Broken code:
time.sleep(0.12) # Long forward pulse even when off center
Fixed code:
pulse = set_speed(err) # Choose pulse based on error
# Smaller pulse when |error| is large prevents overshoot on curves
Why it works: Shorter pulses when off center reduce drift and let corrections happen quickly.
Avoid next time: Tie speed to error and add pauses between corrective moves.
Debugging 6.3.2 – Non‑adaptive speed
Problem: Robot moves at one speed all the time, zig‑zagging or stalling.
Clues: “SPEED:FAST” never changes or “SPEED:SLOW” always prints.
Broken code:
def set_speed(error):
return 0.1 # Fixed pulse (ignores error)
Fixed code:
def set_speed(error):
if abs(error) < 0.5:
return 0.12 # FAST when centered
else:
return 0.06 # SLOW when off center
Why it works: Adaptive speed matches motion to sensor confidence, keeping movement smooth.
Avoid next time: Never ignore error—use it to guide speed and turning.
Final checklist
- L/C/R ADC values print and threshold to clear booleans
- Multi‑sensor rules produce forward and gentle corrections
- Curves confirmed over windows; intersections handled with short pauses
- Speed adapts to error (FAST when centered, SLOW on edges)
- Lost‑line search pattern wiggles safely and re‑acquires the track
Extras
- 🧠 Student tip: Mark your track with consistent tape color and width; re‑calibrate threshold after lighting changes.
- 🧑🏫 Instructor tip: Have students log RAW L/C/R while driving different segments to pick good thresholds.
- 📖 Glossary:
- Threshold: A cut‑off value separating “on” vs “off” detection.
- Window: Consecutive detections used to confirm a state.
- Error: A simple measure of how far off center you are.
- 💡 Mini tips:
- Keep sensors at the same height; uneven mounting causes false turns.
- Insert brief stops before changing direction to prevent stalling.
- Share ground across sensors, driver, and MCU for stable readings.