🏆 Level 7 – Final Integration

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

PartHow many?Pin connection / Notes
D1 R32 (ESP32)1USB cable (30 cm)
Analog grayscale sensor1OUT → Pin 34 (ADC), VCC, GND
RGB LED (common cathode)1R→Pin 14, G→Pin 27, B→Pin 26 (with resistors)
Servo (SG90)1Signal → Pin 13, VCC (5V), GND (shared)
Optional motors (line follow)1 setL298N: 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.
On this page