Automatic light: a photosensitive module

Adjust a photosensitive module's threshold, identify its dark-state output and switch an LED after 50 ms of stable input.

Sensors and measurement displaysBeginner75 min

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

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Light sensor module with digital output1Photoresistor/comparator module verified for 3.3 V; VCC/GND/DO. AO is unused. Verify DO polarity.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires6Use 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.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.3V3U1.VCCModule supply; identify the VCC terminal.
ESP32.GNDU1.GNDCommon ground for the board and sensor.
U1.DOESP32.GPIO273.3 V logic signal to GPIO27.
ESP32.GPIO25R1.1Digital output for the LED.
R1.2D1.AR1, 220 Ω, in series with the LED anode.
D1.KESP32.GNDCathode to common ground.
  1. Disconnect USB. Locate the 3V3, GND, GPIO27 and GPIO25 labels on your board rather than counting header positions.
  2. Connect module VCC to 3V3 and module GND to board GND.
  3. Connect DO to GPIO27. Confirm that the wire is not attached to AO.
  4. Connect GPIO25 through R1, a 220 Ω resistor, to D1's anode. Connect its cathode to common GND.
  5. 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

light_switch.ino Arduino / C++
Download .ino

#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.

ExperimentExpected behaviour
Reset in a clearly illuminated positionThe LED starts off and remains off.
Cover the photoresistorThe LED turns on after stable darkness is accepted.
Keep it coveredThe LED remains on.
Illuminate it againThe LED turns off after stable light is accepted.
Reset while the photoresistor is coveredThe 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

SymptomCheck and action
The LED operates backwardsObserve the raw level in darkness and correct DARK_LEVEL.
The raw level never changesCheck DO versus AO, supply, common ground and the trimmer threshold.
The LED flickers near the boundaryMove the LED away from the sensor; check lighting, threshold and loose contacts.
Accepted darkness does not light the LEDCheck GPIO25, R1, D1 polarity and common ground.
A disconnected input appears darkThe 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.

Downloads

Primary references