Project 7.1: "Color Detector"
What you’ll learn
- ✅ Gray level sensing: Read an analog grayscale sensor and normalize brightness.
- ✅ Color inference by reflectance: Use an RGB light and one sensor to estimate basic colors.
- ✅ Color classification: Build thresholds for RED/GREEN/BLUE and UNKNOWN with confidence scoring.
- ✅ Color‑coded behaviors: Follow tape colors or trigger actions by color tags.
- ✅ Sorting integration: Drive a servo to sort items by detected color with clear prints.
Blocks glossary (used in this project)
- ADC read: 12‑bit analog input for light/reflectance level.
- Normalization: Map 0–4095 to 0.0–1.0 brightness.
- RGB illumination: Drive R/G/B LEDs to probe reflectance by color channel.
- Thresholds: Min/max values to classify channels and decide labels.
- Servo control: PWM to 0°, 90°, 180° for sorting positions.
- Serial println: Print short tags “SENS:…”, “NORM:…”, “RGB:…”, “COLOR:…”, “CONF:…”, “SORT:…”, “FOLLOW:…”.
What you need
| Part | How many? | Pin connection / Notes |
|---|---|---|
| D1 R32 (ESP32) | 1 | USB cable (30 cm) |
| Analog grayscale sensor | 1 | OUT → Pin 34 (ADC), VCC, GND |
| RGB LED (common cathode) | 1 | R→Pin 14, G→Pin 27, B→Pin 26 (with resistors) |
| Servo (SG90) | 1 | Signal → Pin 13, VCC (5V), GND (shared) |
| Optional motors (line follow) | 1 set | L298N: Left IN1→18, IN2→19; Right IN3→5, IN4→23 |
Notes
- Use current‑limiting resistors (e.g., 220–330 Ω) for R/G/B LED pins.
- Keep the sensor at a fixed height over the surface (3–8 mm) for stable readings.
- Share ground across ESP32, LED, servo, sensor, and motor driver.
Before you start
- Sensor wired to Pin 34; RGB LED to 14/27/26; servo to Pin 13
- Place colored test cards (red, green, blue) and a white reference nearby
- Serial monitor open and shows:
print("Ready!") # Confirm serial is working so you can see messages
Microprojects 1–5
Microproject 7.1.1 – Detection of gray levels
Goal: Read the analog sensor, normalize brightness, and print a clean snapshot.
Blocks used:
- ADC read: Light level.
- Normalization: 0–1 value.
MicroPython code:
import machine # Import machine to access ADC and pins
import time # Import time for small delays
adc = machine.ADC(machine.Pin(34)) # Create ADC on Pin 34 for grayscale sensor
adc.atten(machine.ADC.ATTN_11DB) # Set attenuation for wider voltage range
adc.width(machine.ADC.WIDTH_12BIT) # Set 12-bit resolution (0–4095)
print("SENS:ADC READY PIN=34") # Print sensor readiness
def read_gray(): # Define function to read and normalize grayscale value
raw = adc.read() # Read raw ADC count (0–4095)
norm = max(0.0, min(1.0, raw / 4095.0)) # Normalize to 0.0–1.0 and clamp
print("SENS:RAW", raw, "NORM:", round(norm, 3)) # Print raw and normalized values
return norm # Return normalized brightness
val = read_gray() # Take one reading
time.sleep(0.2) # Short delay for readability
Reflection: Normalization turns sensor counts into an easy brightness scale anyone can read.
Challenge:
- Easy: Print a bar “#####” scaled to brightness.
- Harder: Record min/max across 50 samples to auto‑calibrate.
Microproject 7.1.2 – Differentiation of basic colors by RGB illumination
Goal: Light the scene with red, green, and blue LEDs separately and record reflectance.
Blocks used:
- GPIO outputs: Drive R/G/B pins.
- Sequenced sampling: Take time‑spaced readings per channel.
MicroPython code:
import machine # Import machine for Pin outputs
import time # Import time for delays
ledR = machine.Pin(14, machine.Pin.OUT) # Create red LED control on Pin 14
ledG = machine.Pin(27, machine.Pin.OUT) # Create green LED control on Pin 27
ledB = machine.Pin(26, machine.Pin.OUT) # Create blue LED control on Pin 26
print("RGB:PINS R=14 G=27 B=26") # Print LED pin setup
def sample_channel(pin_on): # Define helper to sample sensor under a specific LED
ledR.value(0) # Ensure red LED is OFF
ledG.value(0) # Ensure green LED is OFF
ledB.value(0) # Ensure blue LED is OFF
pin_on.value(1) # Turn ON the selected LED
time.sleep(0.05) # Wait for light to stabilize
val = read_gray() # Read normalized brightness with current illumination
pin_on.value(0) # Turn OFF the selected LED
return val # Return channel brightness
r = sample_channel(ledR) # Sample reflectance under red light
g = sample_channel(ledG) # Sample reflectance under green light
b = sample_channel(ledB) # Sample reflectance under blue light
print("RGB:SAMPLE R", round(r, 3), "G", round(g, 3), "B", round(b, 3)) # Print channel values
Reflection: A single sensor can “guess” colors by how bright they look under different colored lights.
Challenge:
- Easy: Repeat sampling 3× and average each channel.
- Harder: Add ambient measurement (all LEDs OFF) and subtract background.
Microproject 7.1.3 – Classifying objects by color
Goal: Build thresholds for RED/GREEN/BLUE and report label + confidence.
Blocks used:
- Thresholds: Per‑channel comparisons.
- Confidence: Margin between top and second channel.
MicroPython code:
thr = 0.05 # Set minimal difference threshold to consider a dominant color
print("COLOR:THR", thr) # Print threshold setting
def classify_rgb(r, g, b): # Define function to classify color based on channel values
vals = {"RED": r, "GREEN": g, "BLUE": b} # Create dict of channel values
sorted_items = sorted(vals.items(), key=lambda kv: kv[1], reverse=True) # Sort channels by brightness
top_label, top_val = sorted_items[0] # Extract top channel label and value
second_val = sorted_items[1][1] # Extract second highest value
margin = top_val - second_val # Compute margin between top and second
conf = max(0.0, round(margin, 3)) # Compute non-negative confidence score
if conf < thr: # If margin below threshold
label = "UNKNOWN" # Assign unknown label
else: # If margin sufficient
label = top_label # Assign dominant color label
print("COLOR:CLASS", label, "CONF:", conf, "R/G/B:", round(r,3), round(g,3), round(b,3)) # Print classification line
return label, conf # Return label and confidence
label, conf = classify_rgb(r, g, b) # Classify sampled channels
Reflection: Confidence is your honesty meter—small margins mean “not sure,” and that’s okay.
Challenge:
- Easy: Add “WHITE” when all channels are high and balanced.
- Harder: Add “BLACK” when all channels are very low.
Microproject 7.1.4 – Color‑coded line follower (simple)
Goal: React to color tags on a track: RED=LEFT nudge, GREEN=RIGHT nudge, BLUE=FORWARD.
Blocks used:
- Mapping: Color → action.
- Motor helpers: Forward/left/right pulses.
MicroPython code:
import machine # Import machine for motor pins
import time # Import time for pulse durations
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Create left IN1 motor pin
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Create left IN2 motor pin
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Create right IN3 motor pin
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Create right IN4 motor pin
print("MOTORS:READY 18/19 5/23") # Print motor setup
def motors_stop(): # Define function to stop both motors
L_IN1.value(0) # Set left IN1 LOW
L_IN2.value(0) # Set left IN2 LOW
R_IN3.value(0) # Set right IN3 LOW
R_IN4.value(0) # Set right IN4 LOW
print("MOVE:STOP") # Print stop action
def forward(ms=200): # Define forward pulse helper in milliseconds
L_IN1.value(1) # Set left forward HIGH
L_IN2.value(0) # Set left backward LOW
R_IN3.value(1) # Set right forward HIGH
R_IN4.value(0) # Set right backward LOW
print("MOVE:FWD", ms) # Print forward pulse
time.sleep(ms / 1000.0) # Run motors for ms duration
motors_stop() # Stop motors after pulse
def left(ms=160): # Define left turn pulse helper
L_IN1.value(0) # Set left forward LOW
L_IN2.value(1) # Set left backward HIGH
R_IN3.value(1) # Set right forward HIGH
R_IN4.value(0) # Set right backward LOW
print("MOVE:LEFT", ms) # Print left turn pulse
time.sleep(ms / 1000.0) # Run turn for ms
motors_stop() # Stop motors
def right(ms=160): # Define right turn pulse helper
L_IN1.value(1) # Set left forward HIGH
L_IN2.value(0) # Set left backward LOW
R_IN3.value(0) # Set right forward LOW
R_IN4.value(1) # Set right backward HIGH
print("MOVE:RIGHT", ms) # Print right turn pulse
time.sleep(ms / 1000.0) # Run turn for ms
motors_stop() # Stop motors
def react_by_color(label): # Define function to react to color label
if label == "RED": # If red detected
left(140) # Apply a small left nudge
elif label == "GREEN": # If green detected
right(140) # Apply a small right nudge
elif label == "BLUE": # If blue detected
forward(180) # Apply forward pulse
else: # If unknown color
print("FOLLOW:UNKNOWN") # Print unknown reaction
motors_stop() # Stop safely
react_by_color(label) # React to classified color
Reflection: Small nudges keep motion gentle—kids see action tied to colorful cues.
Challenge:
- Easy: Add “YELLOW” behavior as forward‑left.
- Harder: Use confidence to scale pulse (higher confidence = longer pulse).
Microproject 7.1.5 – Integration with sorting system
Goal: Move a servo to bins based on color labels.
Blocks used:
- PWM servo: 0°, 90°, 180° positions.
- Mapping: Color → angle.
MicroPython code:
import machine # Import machine for PWM servo
import time # Import time for delays
servo = machine.PWM(machine.Pin(13), freq=50) # Create PWM on Pin 13 at 50 Hz
print("SORT:SERVO READY PIN=13") # Print servo readiness
def angle_to_duty(angle): # Define function to map angle to PWM duty
us = 500 + int((angle / 180.0) * 2000) # Compute microseconds (0°=500us, 180°=2500us)
duty = int(us * 1023 / 20000) # Convert microseconds to duty (assuming 20 ms period)
print("SORT:DUTY", duty, "ANGLE", angle) # Print duty and angle
return duty # Return duty value
def sort_by_color(label): # Define function to move servo to bin by color
if label == "RED": # If red label
duty = angle_to_duty(0) # Compute duty for 0°
elif label == "GREEN": # If green label
duty = angle_to_duty(90) # Compute duty for 90°
elif label == "BLUE": # If blue label
duty = angle_to_duty(180) # Compute duty for 180°
else: # If unknown label
duty = angle_to_duty(45) # Compute duty for middle fallback
servo.duty(duty) # Apply duty to servo
print("SORT:MOVE", label) # Print sorting move
time.sleep(0.5) # Wait for servo to settle
sort_by_color(label) # Sort based on classified color
Reflection: Sorting turns measurement into a tangible result—“I see red; I place it left.”
Challenge:
- Easy: Blink the LED once in the color of the bin.
- Harder: Add a short “confirm” print with label and confidence.
Main project – Color detector and sorter
Blocks steps (with glossary)
- Gray read + normalization: Clean brightness snapshot.
- RGB probing: R/G/B channel reflectance via LED sequencing.
- Classification: Dominant channel with threshold and confidence.
- Color behaviors: Map label to motion or follow action.
- Sorting: Servo bins by color label, with fallback.
MicroPython code (mirroring blocks)
# Project 7.1 – Color Detector and Sorter (ADC + RGB probe + Classify + React + Servo)
import machine # Import machine for ADC, GPIO, PWM
import time # Import time for delays and sequencing
# ===== Sensor (ADC) =====
adc = machine.ADC(machine.Pin(34)) # Create ADC on Pin 34
adc.atten(machine.ADC.ATTN_11DB) # Set attenuation for full range
adc.width(machine.ADC.WIDTH_12BIT) # Set 12-bit resolution
print("INIT:SENS ADC=34") # Print sensor init
def read_gray(): # Define normalized gray read
raw = adc.read() # Read raw ADC value
norm = max(0.0, min(1.0, raw / 4095.0)) # Normalize 0..1
print("SENS:RAW", raw, "NORM", round(norm, 3)) # Print raw and norm
return norm # Return norm
# ===== RGB illumination =====
ledR = machine.Pin(14, machine.Pin.OUT) # Red LED pin
ledG = machine.Pin(27, machine.Pin.OUT) # Green LED pin
ledB = machine.Pin(26, machine.Pin.OUT) # Blue LED pin
print("INIT:RGB R=14 G=27 B=26") # Print RGB pins
def sample_channel(pin_on): # Define sample under one color
ledR.value(0) # Turn OFF red
ledG.value(0) # Turn OFF green
ledB.value(0) # Turn OFF blue
pin_on.value(1) # Turn ON selected color
time.sleep(0.05) # Stabilize illumination
val = read_gray() # Read brightness
pin_on.value(0) # Turn OFF selected color
return val # Return channel value
def sample_rgb(): # Define full RGB sampling
r = sample_channel(ledR) # Sample red channel
g = sample_channel(ledG) # Sample green channel
b = sample_channel(ledB) # Sample blue channel
print("RGB:VAL R", round(r,3), "G", round(g,3), "B", round(b,3)) # Print RGB values
return r, g, b # Return tuple
# ===== Classification =====
thr = 0.05 # Margin threshold for dominance
print("INIT:THR", thr) # Print threshold
def classify_rgb(r, g, b): # Define classification
vals = {"RED": r, "GREEN": g, "BLUE": b} # Channel dict
sorted_items = sorted(vals.items(), key=lambda kv: kv[1], reverse=True) # Sort by brightness
top_label, top_val = sorted_items[0] # Top label/value
second_val = sorted_items[1][1] # Second value
margin = top_val - second_val # Compute margin
conf = max(0.0, round(margin, 3)) # Confidence score
if conf < thr: # If margin too small
label = "UNKNOWN" # Unknown label
else: # Otherwise dominant
label = top_label # Dominant color label
print("COLOR:CLASS", label, "CONF", conf, "R/G/B", round(r,3), round(g,3), round(b,3)) # Print classification
return label, conf # Return label and confidence
# ===== Motors (optional follow) =====
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") # Print motors ready
def motors_stop(): # Define stop
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 forward(ms=180): # Define forward pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left backward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right backward LOW
print("MOVE:FWD", ms) # Print forward
time.sleep(ms / 1000.0) # Run motors
motors_stop() # Stop motors
def left(ms=140): # Define left pulse
L_IN1.value(0) # Left forward LOW
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right backward LOW
print("MOVE:LEFT", ms) # Print left
time.sleep(ms / 1000.0) # Run motors
motors_stop() # Stop motors
def right(ms=140): # Define right pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left backward LOW
R_IN3.value(0) # Right forward LOW
R_IN4.value(1) # Right backward HIGH
print("MOVE:RIGHT", ms) # Print right
time.sleep(ms / 1000.0) # Run motors
motors_stop() # Stop motors
def react_by_color(label, conf): # Define color behavior with confidence
scale = 1.0 + min(0.5, conf) # Scale pulses by confidence up to +50%
if label == "RED": # Red behavior
left(int(120 * scale)) # Left nudge scaled
elif label == "GREEN": # Green behavior
right(int(120 * scale)) # Right nudge scaled
elif label == "BLUE": # Blue behavior
forward(int(160 * scale)) # Forward scaled
else: # Unknown behavior
print("FOLLOW:UNKNOWN") # Print unknown
motors_stop() # Stop safely
# ===== Servo sorting =====
servo = machine.PWM(machine.Pin(13), freq=50) # Servo PWM at 50 Hz
print("INIT:SERVO PIN=13") # Print servo init
def angle_to_duty(angle): # Map angle to duty
us = 500 + int((angle / 180.0) * 2000) # Microseconds from angle
duty = int(us * 1023 / 20000) # Duty for 20 ms frame
print("SORT:MAP angle", angle, "duty", duty) # Print mapping
return duty # Return duty
def sort_by_color(label): # Move servo to bin
if label == "RED": # Red bin
servo.duty(angle_to_duty(0)) # 0°
elif label == "GREEN": # Green bin
servo.duty(angle_to_duty(90)) # 90°
elif label == "BLUE": # Blue bin
servo.duty(angle_to_duty(180)) # 180°
else: # Unknown bin
servo.duty(angle_to_duty(45)) # Middle fallback
print("SORT:MOVE", label) # Print move
time.sleep(0.5) # Wait to settle
# ===== Main loop =====
print("RUN:Color Detector") # Announce start
while True: # Continuous operation
r, g, b = sample_rgb() # Probe channels
label, conf = classify_rgb(r, g, b) # Classify color
react_by_color(label, conf) # Optional color‑coded behavior
sort_by_color(label) # Move servo to sort
time.sleep(0.2) # Small pacing delay
External explanation
- What it teaches: How to turn a single analog sensor into a “color detector” by illuminating with R/G/B and reading reflectance, then mapping labels to robot actions and sorting with a servo.
- Why it works: Different colors reflect colored light differently; collecting three brightness samples gives a simple fingerprint; thresholds and confidence convert it into reliable, explainable decisions.
- Key concept: “Probe → measure → classify → act.”
Story time
You shine red, then green, then blue—three quick pulses. The sensor whispers back numbers, and suddenly “RED” isn’t just a word; it’s a nudge left and a gentle servo swing into the red bin. Small signals, clear decisions, satisfying motion.
Debugging (2)
Debugging 7.1.1 – Does not distinguish similar colors
Problem: Red vs. orange or teal vs. green gets misclassified.
Clues: Margin (CONF) is small; “UNKNOWN” appears often.
Broken code:
thr = 0.01 # Threshold too low; accepts weak dominance
Fixed code:
thr = 0.05 # Raise dominance threshold for cleaner labels
print("DEBUG:THR", thr) # Verify threshold
Why it works: A higher margin requirement avoids fragile decisions on near‑similar colors.
Avoid next time: Average multiple samples and subtract ambient to sharpen differences.
Debugging 7.1.2 – Lighting conditions affect detection
Problem: Room lights or shadows change readings.
Clues: Ambient (LEDs OFF) is high or variable; RGB:VAL swings widely.
Broken code:
# No ambient subtraction used
val = read_gray()
Fixed code:
amb = read_gray() # Measure ambient with LEDs OFF
val = max(0.0, read_gray() - amb) # Subtract ambient for each channel
print("DEBUG:AMB", round(amb,3)) # Track ambient level
Why it works: Removing ambient makes the readings reflect your LED color rather than the room.
Avoid next time: Shade the sensor area and keep height consistent.
Final checklist
- ADC prints raw and normalized brightness consistently
- RGB probing prints clear channel values for R/G/B
- Classification prints COLOR:CLASS with label and confidence
- Color behaviors nudge motors gently and stop safely on UNKNOWN
- Servo sorting maps labels to 0°/90°/180° positions with printed duty
- Fixes for ambient light and thresholds are ready if conditions change
Extras
- 🧠 Student tip: Make a calibration card: measure R/G/B on white and black to set sane thr and confidence expectations.
- 🧑🏫 Instructor tip: Have teams log 10 samples per color and compute averages; compare the margin across materials.
- 📖 Glossary:
- Reflectance: How much light a surface bounces back under a color channel.
- Dominance (margin): Difference between top and second channel brightness.
- Ambient subtraction: Removing background light from measurements.
- 💡 Mini tips:
- Use consistent resistors for R/G/B so channels are comparable.
- Add a small hood around the sensor to reduce stray light.
- Print short tags only; classroom logs should be easy to scan.