- Draw the sensing-to-output architecture.
- Collect and label representative readings.
- Train a small model without hiding a simpler baseline.
- Separate prediction from the output decision.
- Evaluate behavior before connecting the LED.
Before you begin
Complete the exact LED wiring in PI06, the MCP3008 light divider in PI07 and the local classifier in PI09. This project controls an indicator only—not mains lighting or a safety system.
Choose an honest purpose for the AI
The project reports whether your desk looks dim enough to need more light, according to examples you label. The output is a tiny indicator LED. The Pi reads one normalized sensor value and uses a trained decision tree to propose a class: 1 means “dim,” 0 means “not dim.”
A hand-written threshold could probably do this job just as well. That is important, not embarrassing: the build teaches where a model fits in a physical system, while making it possible to compare it with simpler logic. Do not market a light threshold as general intelligence or claim that a learned model is automatically more reliable.
All processing happens locally. The MCP3008 converts the divider voltage; GPIO Zero reads it; scikit-learn fits and applies the classifier; Python validates the result and controls the LED. No cloud service receives readings. Your eyes observe the indicator as feedback, but the code does not electrically confirm that it actually lit.
Light → LDR divider → MCP3008 → Pi feature value
↓
Local trained classifier
↓
Decision policy → LED
↓
Human checks the outcomeReuse tested subsystems, not assumptions
Use the Pi, ADC, LDR, 10 kΩ divider resistor and SPI connections from PI07. Add the red LED, 1 kΩ series resistor and GPIO17 connections from PI06. These use different GPIO signals, so the stated pin map has no conflict. Keep the indicator physically away from the light sensor so its own light does not alter the input.
For completeness: MCP3008 pins 16 and 15 go to Pi 3.3 V pin 1; ADC 14 and 9 go to ground pin 6. ADC 13/12/11/10 go to Pi physical 23/21/19/24. ADC pin 1 reads the LDR-divider junction. GPIO17 physical 11 goes through 1 kΩ to LED anode, with cathode at ground. Follow both prerequisite diagrams and verify the DIP orientation before power.
Install python3-gpiozero, python3-lgpio and python3-sklearn through Raspberry Pi OS packages, using the system interpreter. SPI must be enabled. First rerun the separate sensor and LED tests; combining two broken subsystems makes diagnosis needlessly difficult. Keep the LED disconnected while evaluating the model-only steps.
| Part | Role |
|---|---|
| Pi 4/5, OS, suitable supply and storage | Runs acquisition, model and policy |
| MCP3008 DIP, LDR and 10 kΩ resistor | Provides the light measurement |
| Red LED and 1 kΩ resistor | Low-current indication only |
| Breadboard and jumper wires | 3.3 V wiring and common ground |
Collect examples you can defend
Create light_training.csv with the header ratio,label. Use PI07 readings to collect at least twenty independently repositioned or relit examples per class. Save each normalized ratio and your chosen label. Do not copy a hundred adjacent readings from one unchanged scene and call them a hundred different lighting conditions.
Keep a separate light_test.csv in the same format, collected in a later session under several conditions. Never add these rows to the fitting file while evaluating this version. Both files should contain only finite ratios between zero and one and labels 0 or 1. The program rejects missing classes or unexpected values in training.
Label the desk condition by your stated criterion before looking at the model’s answer. If people disagree about what counts as dim, write down the rule and examples. The model learns labels, not an objective law about comfortable lighting. Include intermediate conditions and record ambiguity rather than quietly discarding every hard example.
Open both files in a text editor to inspect the header and a few rows. A suitable example row looks like 0.18,1, but do not use that illustrative value as a calibrated threshold. Your divider, desk and room may differ. A comma-separated file can look correct while a spreadsheet has silently changed decimal formatting.
Train, evaluate, then connect the indicator
Save the program as learned_light.py beside the two CSV files. Run it with the output circuit disconnected first. It prints test predictions beside their true labels; calculate correct predictions divided by the number of test rows. If the labels or errors do not make sense, stop and fix the data before reconnecting the indicator.
The model learns its split from examples instead of receiving a threshold chosen in code. A shallow tree bounds complexity. Each runtime prediction still passes through a deterministic decision: only label 1 turns the LED on. The loop is paced, input is checked, and cleanup requests off on exit. This is adequate for a small indicator exercise, not certified fault protection.
The example intentionally avoids persisting executable model files: fitting this tiny dataset at startup is simple. For larger systems, model versioning and trusted artifact loading deserve their own design.
import csv
import math
from time import sleep
from gpiozero import MCP3008, LED
from sklearn.tree import DecisionTreeClassifier
def load_rows(path):
with open(path, newline="") as source:
rows = list(csv.DictReader(source))
x = [[float(row["ratio"])] for row in rows]
y = [int(row["label"]) for row in rows]
if not x or any(not math.isfinite(r[0]) or not 0 <= r[0] <= 1 for r in x):
raise ValueError("Need finite ratios from 0 to 1")
if any(label not in (0, 1) for label in y):
raise ValueError("Labels must be 0 or 1")
return x, y
train_x, train_y = load_rows("light_training.csv")
if set(train_y) != {0, 1}:
raise ValueError("Collect examples of both classes")
model = DecisionTreeClassifier(max_depth=2, random_state=7)
model.fit(train_x, train_y)
test_x, test_y = load_rows("light_test.csv")
print("true/predicted:", list(zip(test_y, model.predict(test_x))))
with MCP3008(channel=0) as sensor, LED(17, initial_value=False) as led:
try:
while True:
ratio = sensor.value
if not math.isfinite(ratio) or not 0 <= ratio <= 1:
raise ValueError("Invalid sensor reading")
label = int(model.predict([[ratio]])[0])
led.value = label == 1
print(round(ratio, 3), label)
sleep(0.5)
except KeyboardInterrupt:
pass
finally:
led.off()load_rows validates file values. The held-out file is never passed to fit. The double brackets provide one sample containing one feature. No arbitrary model text is executed and only the known dim class drives the LED.
Expected result: A list of true/predicted pairs, then live ratio/label lines. With valid wiring the LED indicates class 1. Exact classifications depend on your dataset; stop with Ctrl+C.
Evaluate the system, including its limits
Test bright, dim and borderline scenes while noting the printed value and visible indicator. Compare the learned rule with a threshold chosen using training data only, then evaluate both on the same untouched test session. If the simpler rule is equally useful, prefer it for a real light indicator.
A disconnected sensor can produce a plausible endpoint value, so the range check cannot detect every wiring failure. Software may also freeze while an output is on. Those limits are acceptable only because this output is a low-current indicator. Do not reuse this policy unchanged for heaters, motors, door locks or alarms.
Rapid changes near a class boundary may make the LED flicker. A later modification could require several consistent predictions before switching, or add hysteresis to a threshold baseline. Both introduce a response delay, so test the tradeoff rather than hiding it.
The finish condition is not “the LED came on once.” Keep a short record of your pin map, package versions, collection sessions, held-out errors and stop behavior. This becomes the starting point for the larger AI + Raspberry Pi projects, where interfaces and failure policies matter more than a single impressive demonstration.
Important terms
- Feature
- The measured input used by a model; here a normalized divider reading.
- Label
- The target class assigned to an example.
- Baseline
- A simpler method used as a comparison.
- Decision policy
- Rules translating a prediction into an allowed output.
- Hysteresis
- Different switching boundaries depending on the current state.
Mini project: A documented local light classifier
- Verify the sensor and LED separately, then disconnect the LED for model evaluation.
- Collect and inspect separate training and later-session test files.
- Fit the tree and count test errors before reconnecting output.
- Test bright, dim, ambiguous and stop conditions; compare a threshold baseline and write the limitations.
Common mistakes and debugging
- Giving one unchanged scene many duplicate rows: collect independent conditions and sessions.
- Treating a plausible sensor value as proof of healthy wiring: document undetectable faults.
- Letting the indicator illuminate the sensor: separate their placement.
- Using this LED policy for a high-risk actuator: redesign safety and independent stop mechanisms first.
Independent challenge
Implement a three-consistent-predictions switching rule. Measure how it changes visible flicker and response delay, and keep the original held-out test unchanged.
Check your understanding: 10 questions
Where does inference happen?
What is the single feature?
What does class 1 mean?
Why mention a threshold baseline?
Why collect test data later?
Does the code measure whether the LED actually lights?
Why keep the LED away from the LDR?
What does the range check fail to detect?
Does requiring three consistent predictions have a cost?
What evidence completes the project?
Quiz answers
Reveal all 10 answers after your attempt
- On the Raspberry Pi, without a cloud request.
- The normalized light-divider reading from ADC channel zero.
- The user-labeled dim condition that requests the indicator on.
- It may solve the real task more simply and gives the model a meaningful comparison.
- It better checks transfer to a separate session instead of nearly identical neighboring readings.
- No. It commands the output; a human observes it in this prototype.
- Its light could change the measurement and create unintended feedback.
- A faulty or disconnected circuit that still yields a plausible in-range reading.
- Yes. It adds delay even if it reduces flicker.
- Repeatable subsystem tests, held-out evaluation, a baseline comparison and documented output/stop behavior—not one successful demonstration.
Summary
You have combined sensing, learned inference and a bounded physical output. The best result is a system whose behavior and limits you can explain, including when simpler logic is preferable.
Continue learning
Continue with API01 for an assistant-style project, or use the robotics course to learn how stronger timing and stop requirements change a moving system.
- Build a Raspberry Pi AI Assistant
- How Obstacle-Avoidance Robots Work
- Build a Complete Computer-to-Arduino AI System
Sources and further reading
- Raspberry Pi hardware documentation
- GPIO Zero SPI devices
- scikit-learn DecisionTreeClassifier
- scikit-learn common pitfalls and data leakage
Prepared 2026-09-19. Editorial draft. Primary documentation checked 19 September 2026. Code requires the stated Pi environment; no physical wiring, camera or performance test is claimed.