What you will learn
- Connect a 3.3 V photosensitive module's DO to GPIO27 and an LED through 220 Ω to GPIO25.
- Identify the actual digital-output polarity and configure DARK_LEVEL.
- Adjust the trimmer to distinguish an illuminated photoresistor from a covered one.
- Explain the 50 ms stability check, including initial qualification after reset.
- Distinguish a digital threshold from lux measurement and electrical hysteresis.
Before you start
Complete courses 01, 02 and 03: the Arduino environment, Serial Monitor, an LED with a resistor and filtering button transitions.
Reference setup
- Board in Arduino IDE
- ESP32 Dev Module
- Arduino-ESP32
- 3.3.12
- Serial Monitor
- 115200 baud
This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.
What we will build
Build an automatic light: an LED turns on in darkness and off in sufficient light. A photosensitive module sets the boundary with its trimmer. The ESP32 requires 50 ms of stable input before changing the LED.
Observe the electrical output, then assign meaning in software. Record your checks to create a repeatable experiment.
Learning objectives
- Connect a photosensitive module to an ESP32 digital input.
- Identify which logic level means darkness on your module.
- Adjust the hardware threshold and check both conditions.
- Distinguish the raw input, accepted state and LED state.
- Explain which changes a 50 ms stability check can reject.
Prerequisites
Complete courses 01, 02 and 03: uploading sketches, Serial Monitor, an LED with a resistor, and filtering pushbutton changes. We now apply that timing method to a sensor.
Equipment
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| Light sensor module with digital output | 1 | Photoresistor/comparator module verified for 3.3 V; VCC/GND/DO. AO is unused. Verify DO polarity. |
| Red LED | 1 | Anode A and cathode K; identify polarity on the actual part. |
| 220 Ω resistor | 1 | One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors. |
| Jumper wires | 6 | Use male-to-male or female-to-male leads to suit the board headers. Approximate quantity, depending on layout and connector types. |
Use a module documented for 3.3 V operation with a digital output labelled DO or D0. Leave AO disconnected if it exists. The photograph does not identify the exact module. This wiring targets classic ESP32/WROOM-32; C3, S2 and S3 need adaptation.
How the digital decision is made
A photoresistor changes resistance with light, while a comparator evaluates its signal against a threshold. The SunFounder reference module supports 3.3–5 V and outputs LOW above its light threshold and HIGH below it. Its trimmer adjusts that threshold. Check your module against this reference.
DO has two possible states. It does not provide lux readings or distinguish several light levels that all fall on the same side of the threshold. Turning the trimmer moves the decision boundary; it does not change a measurement unit.
The sketch initially sets DARK_LEVEL to HIGH. GPIO27 uses INPUT_PULLUP, enabling an internal pull-up as described in the Espressif GPIO documentation. This is not voltage conversion: power the module from 3V3 and never connect a 5 V signal to the GPIO.
Wiring and assembly
Wiring diagram
The reference module works at 3.3 V. Use DO; leave any AO unconnected. DARK_LEVEL defaults to HIGH; confirm polarity by illuminating and covering the sensor. Terminal labels describe functions, not physical order.
| From | To | Connection |
|---|---|---|
ESP32.3V3 | U1.VCC | Module supply; identify the VCC terminal. |
ESP32.GND | U1.GND | Common ground for the board and sensor. |
U1.DO | ESP32.GPIO27 | 3.3 V logic signal to GPIO27. |
ESP32.GPIO25 | R1.1 | Digital output for the LED. |
R1.2 | D1.A | R1, 220 Ω, in series with the LED anode. |
D1.K | ESP32.GND | Cathode to common ground. |
- Disconnect USB. Locate the 3V3, GND, GPIO27 and GPIO25 labels on your board rather than counting header positions.
- Connect module VCC to 3V3 and module GND to board GND.
- Connect DO to GPIO27. Confirm that the wire is not attached to AO.
- Connect GPIO25 through R1, a 220 Ω resistor, to D1's anode. Connect its cathode to common GND.
- Check breadboard contact groups and LED polarity before reconnecting USB.
Position the LED away from the photoresistor. Its light could otherwise change the measured condition and cause repeated switching. Resolve this feedback through placement before adjusting software.
Arduino environment
In Arduino IDE 2.x, select ESP32 Dev Module and your board's port. This package targets esp32 by Espressif Systems 3.3.12; see the official installation guide. No additional libraries are required.
Extract the Arduino example and open light_switch/light_switch.ino, keeping the folder and sketch names identical. Click Verify, then Upload, and open Serial Monitor at 115200 baud. GPIO25 controls the LED and GPIO27 reads the module.
Complete Arduino sketch
#include <Arduino.h>
// Classic ESP32/WROOM-32. Use a comparator module rated for 3.3 V.
constexpr uint8_t INPUT_PIN = 27;
constexpr uint8_t LED_PIN = 25;
constexpr int DARK_LEVEL = HIGH; // Change to LOW if your module reports LOW in darkness.
constexpr uint32_t FILTER_MS = 50;
constexpr uint32_t REPORT_MS = 500;
int candidateLevel = HIGH;
uint32_t candidateSince = 0;
uint32_t lastReportAt = 0;
bool acceptedKnown = false;
bool acceptedDark = false;
void setup() {
Serial.begin(115200);
pinMode(INPUT_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
candidateLevel = digitalRead(INPUT_PIN);
candidateSince = millis();
lastReportAt = candidateSince;
Serial.println("Light switch: qualify DO for 50 ms; confirm DARK_LEVEL with your module.");
}
void loop() {
const uint32_t now = millis();
const int raw = digitalRead(INPUT_PIN);
if (raw != candidateLevel) {
candidateLevel = raw;
candidateSince = now;
}
if (static_cast<uint32_t>(now - candidateSince) >= FILTER_MS) {
acceptedKnown = true;
acceptedDark = (candidateLevel == DARK_LEVEL);
}
digitalWrite(LED_PIN, acceptedKnown && acceptedDark ? HIGH : LOW);
if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
lastReportAt = now;
Serial.print("raw="); Serial.print(raw);
Serial.print(" accepted=");
Serial.print(!acceptedKnown ? "WAITING" : (acceptedDark ? "DARK" : "LIGHT"));
Serial.print(" LED="); Serial.println(acceptedKnown && acceptedDark ? "ON" : "OFF");
}
}
The displayed and downloadable sketches match. Initially retain DARK_LEVEL = HIGH.
Understanding the program
In setup(), the LED starts off and the program reads the initial input. This first reading is not yet a qualified condition: it must remain unchanged for at least 50 ms, just like later changes.
FILTER_MS sets 50 ms, candidateLevel stores the candidate, and candidateSince marks its stability start. acceptedKnown tracks initial qualification; acceptedDark represents accepted darkness. Each raw change in loop() restarts qualification. The program accepts a new level only after the interval has elapsed. The LED turns on when the accepted level matches DARK_LEVEL. Until then, it keeps its previous state. Timing uses the difference between millis() timestamps without blocking the loop for 50 ms.
Every 500 ms, Serial shows raw as 0/1 for LOW/HIGH, accepted as DARK/LIGHT or initial WAITING, and LED as ON/OFF. Reporting is slower than sampling, so a brief pulse can occur between messages. A steady-looking log therefore cannot prove that the raw input never changed briefly.
Establishing polarity and threshold
Keep the module under normal room light for several seconds, then cover the photoresistor. Observe the raw value in both conditions. If it stays unchanged, adjust the trimmer gradually until illuminated and covered conditions produce distinct levels. Do not force the trimmer beyond its mechanical stop.
If the covered module reads HIGH, retain DARK_LEVEL = HIGH. If your verified module reads LOW in darkness, change that constant to LOW and upload again. Describe the trimmer position, room conditions and light-source position in your notes so you can repeat the experiment.
Running the experiment
With polarity configured, hold each condition for two seconds to obtain several reports.
| Experiment | Expected behaviour |
|---|---|
| Reset in a clearly illuminated position | The LED starts off and remains off. |
| Cover the photoresistor | The LED turns on after stable darkness is accepted. |
| Keep it covered | The LED remains on. |
| Illuminate it again | The LED turns off after stable light is accepted. |
| Reset while the photoresistor is covered | The LED starts off, then turns on after initial qualification. |
Copy the table into your notes and add actual observations. These are predictions from the program, not claimed measurements from your equipment.
Stability and limitations
The filter rejects a new level that fails to remain unchanged for 50 ms. Changes near the threshold can still switch the LED repeatedly if each condition lasts longer. This is a time-based stability test, not electrical hysteresis or two separate light thresholds. Use clearly bright and clearly dark conditions for the initial experiment.
Troubleshooting
| Symptom | Check and action |
|---|---|
| The LED operates backwards | Observe the raw level in darkness and correct DARK_LEVEL. |
| The raw level never changes | Check DO versus AO, supply, common ground and the trimmer threshold. |
| The LED flickers near the boundary | Move the LED away from the sensor; check lighting, threshold and loose contacts. |
| Accepted darkness does not light the LED | Check GPIO25, R1, D1 polarity and common ground. |
| A disconnected input appears dark | The pull-up can produce HIGH; inspect the connection instead of treating it as proof of a working sensor. |
Independent challenge
Change FILTER_MS from 50 to 1000 ms. Cover the photoresistor for approximately half a second, then for two seconds. Predict whether the LED will respond and when. Test uncovering as well: qualification applies in both directions. Restore 50 ms afterwards.
Questions and worked guidance
Why does DO = HIGH not universally mean darkness? Its meaning depends on the module circuit and configured threshold; observation establishes the connection between an electrical level and a physical condition.
With a 1000 ms interval, half a second at the changed level cannot pass the filter. Two seconds can, after approximately one second of stability. The same rule applies when light returns. Changing DARK_LEVEL reverses the interpretation but neither moves the hardware threshold nor creates a lux measurement.
