Automatic light: darkness and motion together

Combine a digital light module and PIR in an automatic LED light. Learn to filter the illumination threshold, extend the hold with motion and clear an earlier event when the room becomes bright.

Integrated projects and user controlsIntermediate90 min

What you will learn

  • Combine darkness and motion conditions to switch an LED on.
  • Verify DO polarity and qualify changes for at least 100 ms.
  • Extend a 10 s hold with each observed PIR HIGH.
  • Cancel the hold when brightness is accepted.
  • Distinguish PIR output from reliable proof of occupancy.

Before you start

Courses 07 and 09: light module, PIR sensor, digital input readings and observations in Serial Monitor.

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

We will build a model of an automatic light that responds to darkness and motion together. A yellow LED represents the light. A PIR sensor reports motion, while the digital output of a photoresistor module tells us whether illumination has crossed an adjustable threshold. Motion in a bright room does not turn the LED on. In darkness, motion turns it on, and the light remains on for another ten seconds after the motion signal ends.

The program also handles a less obvious case: when the room becomes bright, an earlier hold must be cancelled. Returning to darkness must then leave the LED off unless a PIR signal is currently present. This combines two conditions, time filtering and event memory in one small project. The LED is the demonstration load; this lesson does not connect actual room lighting.

What you will learn

  • Combine digital inputs so that both required conditions must be true.
  • Distinguish a raw module signal from an accepted, stable state.
  • Keep an output active for a period that subsequent motion can extend.
  • Clear a remembered event when another required condition ends.
  • Verify digital output polarity instead of assuming what HIGH means.
  • Explain the limitations of PIR detection and a digital light threshold.

Prerequisites

Complete course 07 on the light sensor module and course 09 on the PIR sensor first. You should distinguish power terminals from GPIO pins, connect an LED through a series resistor and read Serial Monitor messages. Test both sensors separately before combining them. When one module behaves unexpectedly on its own, a more complex sketch cannot fix an incorrect connection or an unsuitable threshold setting.

Required 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.
HC-SR501 PIR motion sensor1Module supplied from the board USB 5 V rail; OUT is 3.3 V. Verify pin order, H/L jumper and output pulse duration.
Light sensor module with digital output1Photoresistor/comparator module verified for 3.3 V; VCC/GND/DO. AO is unused. Verify DO polarity.
Yellow LED1Anode A and cathode K.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires12Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity; depends on layout and connector types.

Use a classic ESP32-WROOM-32 board, the reference HC-SR501 and a photoresistor module with VCC, GND and DO terminals. Leave its analogue output AO unconnected. The light module must support a 3.3 V supply and must not drive an excessive voltage into the ESP32 input. The HC-SR501 in this example uses the verified USB 5 V rail and provides a 3.3 V logic output.

The kit photograph does not establish the pin order, comparator marking or output polarity of every supplied module. Check the markings on your actual parts. This lesson requires no additional transistor or diode. A yellow LED and a 220 Ω resistor form the only output circuit.

Two conditions and three timing rules

For the first 60 seconds, the sketch keeps the LED off while the PIR settles. The light filter runs during warm-up too: it accepts a new level only after 100 ms without a change, retaining the previous accepted state until then. This reduces responses to brief transitions near the threshold. The reference configuration is #define DARK_LEVEL HIGH: covering the photoresistor should produce HIGH, while illuminating it should produce LOW. Change this constant when your verified module behaves in the opposite way.

Condition after warm-upLED behaviour
The accepted light state is bright.Off; the previous hold is cancelled.
Dark with PIR HIGH.On; the hold reference time is continually refreshed.
Dark with PIR LOW and an active hold shorter than 10 s.Remains on.
Dark with PIR LOW, with no active hold or after it expires.Off.

A PIR responds to changes in infrared radiation within its field of view. Its output can remain high after a person stops or leaves, depending on the module settings. Our ten-second interval starts from the last observed HIGH, so it adds to the behaviour of the module. An LED turning off does not prove that nobody is present.

Wiring diagram and connections

Wiring diagram

PIR OUT is on GPIO33 and light-sensor DO on GPIO32. The PIR uses the verified USB5V rail; the light module uses 3V3. DARK_LEVEL is HIGH for the reference; verify actual polarity. Keep the indicator LED from illuminating the light sensor.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.USB5VU1.VCCHC-SR501: verified 5 V rail derived from the board USB supply.
ESP32.GNDU1.GNDCommon ground.
U1.OUTESP32.GPIO33PIR OUT, 3.3 V logic, to GPIO33.
ESP32.3V3U2.VCCLight sensor module verified for 3.3 V.
ESP32.GNDU2.GNDCommon ground.
U2.DOESP32.GPIO32Digital output; verify the darkness polarity. Leave AO unconnected.
ESP32.GPIO25R1.1LED output (yellow) to its 220 Ω resistor.
R1.2D1.ASeries resistor to anode A.
D1.KESP32.GNDCathode K to ground.
  1. Disconnect USB. Identify 3V3, GND and the verified USB 5 V rail on your board. GPIO25 does not mean the twenty-fifth physical connector position.
  2. Connect both sensors' GND terminals and the LED cathode to the ESP32 common ground.
  3. Connect light module U2 VCC to 3V3, and DO to GPIO32. Leave AO unconnected.
  4. Connect PIR module U1 VCC to the verified USB 5 V rail, and OUT to GPIO33. Check that OUT is compatible with 3.3 V logic.
  5. Connect GPIO25 → R1, 220 Ω → yellow LED D1 anode. Connect its cathode to GND.
  6. Inspect the whole circuit, especially the separate 5 V PIR supply and 3.3 V light module supply. Only then reconnect USB.

Position the photoresistor so that the indicator LED does not illuminate it. Otherwise optical feedback can produce a cycle: the LED turns on, the sensor sees brightness, the program turns the LED off, and the sensor sees darkness again. Do not connect another independent 5 V supply in parallel with the board's USB power.

Preparing the Arduino environment

In Arduino IDE 2.x, select esp32 by Espressif Systems 3.3.12, ESP32 Dev Module and the correct port. Open dark_motion_light/dark_motion_light.ino. No additional libraries are required. Set Serial Monitor to 115200 baud.

First observe the raw input while illuminating and covering the photoresistor. Adjust the module trimmer so that these two situations produce clearly different levels. If darkness produces LOW, set DARK_LEVEL to LOW, upload again and repeat the check. This threshold is a comparator setting; a DO reading is not a measurement in lux.

Complete Arduino sketch

dark_motion_light.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t PIR_PIN = 33;
constexpr uint8_t LIGHT_PIN = 32;
constexpr uint8_t LED_PIN = 25;
#ifndef DARK_LEVEL
#define DARK_LEVEL HIGH  // Change to LOW only after checking your module.
#endif
constexpr uint32_t WARMUP_MS = 60000;
constexpr uint32_t LIGHT_FILTER_MS = 100;
constexpr uint32_t HOLD_MS = 10000;
constexpr uint32_t REPORT_MS = 500;
bool lightRawHigh = true;
bool lightStableHigh = true;
bool lightStableValid = false;
bool warmingUp = true;
bool holdActive = false;
bool ledOn = false;
uint32_t bootAt = 0;
uint32_t lightChangedAt = 0;
uint32_t lastMotionAt = 0;
uint32_t lastReportAt = 0;

void updateLightFilter(uint32_t now) {
  const bool readingHigh = digitalRead(LIGHT_PIN) == HIGH;
  if (readingHigh != lightRawHigh) {
    lightRawHigh = readingHigh;
    lightChangedAt = now;
  }
  if (static_cast<uint32_t>(now - lightChangedAt) >= LIGHT_FILTER_MS) {
    lightStableHigh = lightRawHigh;
    lightStableValid = true;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(PIR_PIN, INPUT);
  pinMode(LIGHT_PIN, INPUT);
  bootAt = lightChangedAt = lastReportAt = millis();
  lightRawHigh = lightStableHigh = digitalRead(LIGHT_PIN) == HIGH;
  Serial.println("Dark + motion light: PIR warmup 60 s, LED OFF.");
  Serial.println("Check DO polarity; cover the light sensor and observe DO.");
}

void loop() {
  const uint32_t now = millis();
  updateLightFilter(now);
  const bool pirHigh = digitalRead(PIR_PIN) == HIGH;
  const bool dark = lightStableValid &&
                    lightStableHigh == (DARK_LEVEL == HIGH);
  if (warmingUp && static_cast<uint32_t>(now - bootAt) >= WARMUP_MS) {
    warmingUp = false;
    holdActive = false;  // Motion from the warmup period is never replayed.
    Serial.println("Warmup complete.");
  }
  if (warmingUp || !lightStableValid || !dark) {
    holdActive = false;
    ledOn = false;
  } else {
    if (pirHigh) {
      holdActive = true;
      lastMotionAt = now;  // Each observed HIGH extends the hold.
    } else if (holdActive && static_cast<uint32_t>(now - lastMotionAt) >= HOLD_MS) {
      holdActive = false;
    }
    ledOn = holdActive;
  }
  digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
  if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
    lastReportAt = now;
    Serial.print("DO="); Serial.print(lightRawHigh ? "HIGH" : "LOW");
    Serial.print(" stable="); Serial.print(lightStableValid ? (lightStableHigh ? "HIGH" : "LOW") : "WAIT");
    Serial.print(" dark="); Serial.print(dark ? "YES" : "NO");
    Serial.print(" PIR="); Serial.print(pirHigh ? "HIGH" : "LOW");
    Serial.print(" phase="); Serial.print(warmingUp ? "WARMUP" : "READY");
    Serial.print(" LED="); Serial.println(ledOn ? "ON" : "OFF");
  }
}

For the first trial, keep the 60 s warm-up, 100 ms light input stability interval and 10 s hold after the PIR signal. Click Verify, then Upload.

Understanding the sketch

updateLightFilter() separately tracks the raw light input, the time of its latest transition and the accepted light state. Every raw transition restarts the stability interval. Only 100 uninterrupted milliseconds allow a change in the accepted meaning of dark or bright. lightStableValid confirms that at least one level has passed this check. A brief shadow therefore need not change the output.

Warm-up overrides all turn-on rules. Motion during that period is not carried forward as an active hold. Afterwards, a PIR HIGH in accepted darkness activates the hold and refreshes the last observed motion time on every loop iteration.

A separate Boolean flag, holdActive, records whether a hold actually exists, while lastMotionAt stores the last observed HIGH. A timestamp initially set to zero would not be enough: the first ten seconds after startup must not create an imaginary earlier motion event. Accepting brightness clears the hold flag. If darkness returns while the PIR remains LOW, there is no event that can turn the LED on.

The intervals use subtraction of millis() values stored as uint32_t, without long blocking pauses. Serial output approximately every 500 ms is for observation; its frequency does not set the LED response time. Distinguish the sensor transition, the filter's acceptance of it and the later message that reports the resulting state.

Experiments and observations

Reset the board, wait for the initial 60 seconds and observe the sensor without moving it. Where possible, select a short HC-SR501 hold setting according to its documentation. For every experiment, record raw DO, accepted illumination, PIR level and LED state.

ExperimentExpected result and explanation
Move in a bright room.The LED stays off because the darkness condition is false.
Cover the sensor for more than 100 ms and create motion.The LED turns on once both conditions are accepted.
Let PIR become LOW while darkness continues.The LED stays on for about 10 s from the last observed HIGH.
Create another PIR HIGH during the hold.The hold reference time is refreshed.
Illuminate the sensor during the hold.After 100 ms of stable brightness, the LED turns off and its hold memory is cleared.
Restore darkness while PIR remains LOW.The old hold does not return; the LED stays off.
Illuminate the sensor for only 50 ms.The accepted state stays unchanged if no new level lasts long enough.

These are expected results to check during the workshop, not a report of completed physical testing. With messages spaced 500 ms apart, a short raw transition might not appear in the log even though the loop processed it.

Common problems

SymptomWhat to check
The LED works only in bright conditions.Compare actual DO in darkness with DARK_LEVEL; change the constant if polarity is reversed.
The LED repeatedly switches itself on and off.Move the photoresistor away from the LED and inspect the comparator threshold.
The LED does not turn off ten seconds after someone leaves.Inspect PIR level: its internal timer may still be holding HIGH.
PIR produces events immediately after power-up.Wait for warm-up and check placement, the module and its supply.
Darkness and motion never produce light.Inspect both inputs, LED polarity, its series resistor and common ground.
Darkness shortly after brightness does not turn the LED on.This is expected when the hold was cleared and PIR remains LOW.

Independent challenges

  1. Shorten the software hold from 10 s to 3 s. Keep warm-up and light filtering unchanged.
  2. Trace this example: in darkness, the last PIR HIGH is observed at 80 s; accepted brightness occurs at 82 s; darkness returns at 83 s while PIR is LOW. Find the LED state after each event.
  3. Explain why a 100 ms filter is different from hysteresis with two separate illumination thresholds.

Worked solutions

Change HOLD_MS from 10000 to 3000, giving constexpr uint32_t HOLD_MS = 3000;. If the last HIGH was observed at 80 s and darkness continues, the LED turns off at approximately 83 s. This does not shorten the PIR module's internal output pulse; it shortens only the extra interval implemented by our sketch.

In the second example, the LED is on at 80 s. At 82 s, it turns off as soon as the sketch accepts brightness, and the active hold is cleared. At 83 s, it remains off: darkness returning is not a new motion signal. The same result applies with the original ten-second hold.

A time filter requires a digital level to last long enough. Hysteresis would use different entry and exit thresholds, such as two distinct levels of illumination. The digital DO signal does not provide a numerical illumination measurement from which this sketch could define those two thresholds independently.

Check your understanding

Does HIGH always mean dark? No. Its meaning depends on the module and configuration; the constant must match observed behaviour.

Why not use only dark && motion? That expression would turn the LED off as soon as PIR became LOW. Extra memory enables the hold.

What happens during continuous PIR HIGH in darkness? The LED stays on, and the reference time for the additional hold keeps moving forward.

Does an unlit LED confirm that nobody is present? No. PIR responds to changes, and a stationary person may not be reported.

Why does accepted brightness clear earlier memory? This prevents an old event from turning the LED on again after a later dark transition without a current PIR signal.

Primary sources

Downloads