Analog Grayscale

🎮 The Suspects Line Up
Module Goal: measure how much light a surface reflects and discover that between white and black lives a whole civilization of grays: paper, cardboard, felt, fabric. Today your robot learns to judge materials by their tone.
Gather your suspects: a piece of white paper, a piece of cardboard, and something dark (black felt, dark fabric or black paper).
Goal:
measure the reflection of each material and record your evidence.
Concepts:
Analog Input · Reflectivity · Evidence Collection
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc39 as an analog input for the grayscale sensor
# adc39 corresponds to A5 on the Sensor Shield V5.0
adc39 = machine.ADC(machine.Pin(39))
adc39.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
while True: # Repeat forever
value = adc39.read_u16() # Ask how much light the surface reflects (0 to 65535)
print("Material value:", value) # Send the raw evidence to the PC
time.sleep_ms(300) # Pause so the screen stays readableSerial Monitor (hold the sensor 5 mm over each suspect, one at a time):
Material value: 62400 ← white paper (reflects almost everything!)
Material value: 61900
Material value: 38700 ← cardboard (half the light comes back)
Material value: 39200
Material value: 6100 ← black felt (swallows the light)
Material value: 5900
💡 Note: the same infrared trick as the line follower (Module 14) but with a mission upgrade: instead of two extremes, you now have a spectrum. White, cardboard, black: three distinct neighborhoods of numbers. Write all three in your notebook these are the calibration points for the whole module. And notice the sensor's etiquette: it wants the surface 5–10 mm away, held still. Measuring discipline is part of the job.
🎮 Three Tones
Goal:
classify any surface into LIGHT / GRAY / DARK using YOUR three evidence values.
Concepts:
Multi-Zone Thresholds · Calibrated Boundaries
Calibrate: paper ~62000, cardboard ~39000, felt ~6000. Boundaries between neighbors: below 20000 = DARK, above 50000 = LIGHT, between = GRAY. Tune with your own numbers.
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc39 as an analog input for the grayscale sensor
# adc39 corresponds to A5 on the Sensor Shield V5.0
adc39 = machine.ADC(machine.Pin(39))
adc39.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
while True: # Repeat forever
value = adc39.read_u16() # Raw reflectivity reading
print("Material value:", value) # Always show the raw evidence
if value > 50000: # Near your white-paper evidence?
print("Tone: LIGHT bright material") # Light zone
elif value < 20000: # Near your black-felt evidence?
print("Tone: DARK light absorber") # Dark zone
else: # The neighborhood in between
print("Tone: GRAY the middle kingdom") # Gray zone
time.sleep_ms(300) # Pause between verdictsSerial Monitor:
Material value: 61300
Tone: LIGHT bright material
Material value: 37600
Tone: GRAY the middle kingdom
Material value: 6200
Tone: DARK light absorber
💡 Note: three zones need only two fences a rule worth memorizing: N zones, N−1 thresholds. And look at the boundary placement: 50000 sits comfortably between paper and cardboard, 20000 between cardboard and felt each fence in the middle of its gap, with safety margin on both sides (the midpoint rule from 14.2, now applied twice). Test materials beyond your three suspects: a yellow sticky note? Your hand? Where do they land?
🎮 The Sliding Trend
Goal:
slide the sensor across materials and report the direction getting darker or getting lighter.
Concepts:
Delta · Dead Zone · Trend on the Move
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc39 as an analog input for the grayscale sensor
# adc39 corresponds to A5 on the Sensor Shield V5.0
adc39 = machine.ADC(machine.Pin(39))
adc39.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
previous = adc39.read_u16() # First reading = our starting point
while True: # Watch forever
value = adc39.read_u16() # Fresh reading
change = value - previous # The DELTA: how far did the tone move?
if change > 1500: # Clearly MORE reflection than before?
print("Getting LIGHTER (+", change, ")") # Moving toward white
elif change < -1500: # Clearly LESS reflection?
print("Getting DARKER (", change, ")") # Moving toward black
previous = value # Save this reading for the next comparison
time.sleep_ms(100) # Quick checking for smooth slidesSerial Monitor (slide slowly from paper onto cardboard, then onto felt):
Getting DARKER ( -18200 )
Getting DARKER ( -23400 )
Getting LIGHTER ( +6100 )
Getting DARKER ( -29800 )
💡 Note: the delta pattern's second appearance (soil 15.4) and the dead zone grew from 800 to 1500. Why? Because here the sensor is in motion, and your hand wobbles the distance, which wobbles the reading. The faster the context, the wider the dead zone must be noise tolerance is not a constant, it's a design decision you make per situation. Notice also what the delta does NOT say: it never names the material, only the direction of travel. Trend and identity are different questions, needing different tools.
🎮 The Official Table of Materials
Goal:
build a proper measurement protocol each material gets 5 samples, an average, and a verdict. This is how real labs measure: never trust one reading.
Concepts:
Accumulator · Averaging · Measurement Protocol
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc39 as an analog input for the grayscale sensor
# adc39 corresponds to A5 on the Sensor Shield V5.0
adc39 = machine.ADC(machine.Pin(39))
adc39.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
print("=== MATERIAL TABLE ===") # Protocol title
print("We will measure 3 materials, 5 samples each") # Explain the procedure
for material in range(1, 4, 1): # Three materials in total
material = input(('Place material ' + ' under the sensor and press Enter... ')) # Wait for YOU to be ready
total = 0 # The accumulator starts empty
for i in range(1, 6, 1): # Five samples: i counts 1..5
total += adc39.read_u16() # Add one reading to the total
time.sleep_ms(150) # Tiny pause between samples
average = total // 5 # The calm, trustworthy value
print("Material", material, "average:", average) # Report the evidence
if average > 50000: # Near the paper zone?
print("Verdict: LIGHT material") # The verdict
elif average < 20000: # Near the felt zone?
print("Verdict: DARK material") # The verdict
else: # The gray neighborhood
print("Verdict: GRAY material") # The verdict
print("Table complete! Case closed.") # Grand endingSerial Monitor:
=== MATERIAL TABLE ===
We will measure 3 materials, 5 samples each
Place material 1 under the sensor and press Enter...
Material 1 average: 62180
Verdict: LIGHT material
Place material 2 under the sensor and press Enter...
Material 2 average: 38940
Verdict: GRAY material
Place material 3 under the sensor and press Enter...
Material 3 average: 6020
Verdict: DARK material
Table complete! Case closed.
💡 Note: two professional habits live here. ① The averaging protocol: 5 samples instead of 1 the same smoothing you learned on light (13.4), now standard procedure. ② The input() handshake: the program waits for you to place each material the human and the program take turns, like a lab assistant saying "ready when you are." This turn-taking structure (measure on demand) is how real quality-control stations work on factory lines. Compare your averages with your raw readings from 17.1: the verdicts agree, but the numbers are calmer that calm is worth 5 extra lines of code.
🎮 The Darkness Championship
Goal:
put three materials in a competition the program measures each one, remembers the darkest so far, and crowns the CHAMPION OF DARKNESS at the end.
Concepts:
Best-So-Far Tracking · Competition Logic · Round-Based Programs
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc39 as an analog input for the grayscale sensor
# adc39 corresponds to A5 on the Sensor Shield V5.0
adc39 = machine.ADC(machine.Pin(39))
adc39.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
print("=== CHAMPIONSHIP OF DARKNESS ===") # Mission title
print("Three materials enter. The darkest one wins!") # The rules
champion = "" # The name of the current champion
record = 999999 # Start with an impossible LOW value (lower = darker)
for round in range(1, 4, 1): # Three competing rounds
name = input("Name material " + str(round) + ": ") # YOU name the challenger
x = input('Place it under the sensor and press Enter... ') # You place it
total = 0 # Fresh accumulator for this challenger
for i in range(1, 6, 1): # Five official samples
total += adc39.read_u16() # Collect the evidence
time.sleep_ms(150) # Calm sampling
average = total // 5 # The official score
print(name, "scores:", average) # Announce the score
if average < record: # Darker than the current champion?
record = average # New record!
champion = name # New champion crowned!
print(">>>", name, "takes the lead!") # Drama
else: # Not darker than the champion
print("Not dark enough... the champion stands!") # Respect
print("================================") # Drum roll line
print("CHAMPION OF DARKNESS:", champion) # The final verdict
print("Winning score:", record) # The winning numberSerial Monitor:
=== CHAMPIONSHIP OF DARKNESS ===
Three materials enter. The darkest one wins!
Name material 1: Paper
Place it under the sensor and press Enter...
Paper scores: 62180
Not dark enough... the champion stands!
Name material 2: Cardboard
Place it under the sensor and press Enter...
Cardboard scores: 38940
>>> Cardboard takes the lead!
Name material 3: Black felt
Place it under the sensor and press Enter...
Black felt scores: 6020
>>> Black felt takes the lead!
CHAMPION OF DARKNESS: Black felt
Winning score: 6020
💡 Note: the best-so-far pattern finally gets its starring role the same logic that will find the closest wall (Module 19) and the strongest shake (Module 21). Notice the one twist that makes it work here: "best" means **LOWEST** value, so the comparison flips (`<` instead of `>`), and the impossible starting value becomes 999999 instead of 0. A pattern isn't memorized it's *adapted: direction of "better" is a dial you turn per mission. Run the championship with surprise materials (a coin? your sleeve? the desk?) and see who dethrones the felt. Case closed and the analog family with it.
