Project
Project: A Night Light That Turns On with Movement in the Dark, with the ESP32-C3 Super Mini
Step-by-step hallway light with a PIR sensor, an LDR and an LED on the ESP32-C3 Super Mini: it fades on when someone passes in the dark and fades out slowly.
By Prof. Rodrigo Faustino · Updated on · 11 min read
Metrics for this article
- Words
- 2,096
- Characters
- 11,358
- Reading time
- ~11 min
Anyone who has got up in the middle of the night for a glass of water knows the problem: turning on the hallway light dazzles, and walking in the dark means stubbing a toe. This project solves it with a smart night light: it only turns on when it is dark and someone passes by, it turns on gently, stays on while there is movement and then fades out slowly. In the daytime, it never turns on.
It is a small project, with parts from the simulator's bench — a PIR presence sensor, an LDR and an LED —, but it brings together three different ways for the board to talk to the world: a digital input, an analog input and a PWM output. And the program is the perfect example of a state machine, the subject that separates programs that "almost work" from programs that always work.
The board is the ESP32-C3 Super Mini, which fits in a small box behind a baseboard or a piece of furniture.
What you will need
- an ESP32-C3 Super Mini (datasheet);
- a PIR HC-SR501 presence sensor (datasheet);
- an LDR and a 10 kΩ resistor (LDR datasheet);
- an LED (warm white looks nice) and a 220 Ω resistor (LED datasheet);
- breadboard, wires and, for the final version, a small box and a phone USB charger.
From the syllabus to a planned semester
- Teaching plan and calendar built automatically
- Class diary ready to paste into SIGA
- Slides and exercise lists with answer keys
Free Community edition · Windows
Step 1: the wiring
The PIR is powered with 5 V, from the Super Mini's 5V pin, but the OUT output already comes out at 3.3 V, as the PIR lesson explains — so it goes straight to GPIO1, with no divider.
The LDR forms a voltage divider with the 10 kΩ resistor: the LDR goes from 3V3 to the middle point, the resistor goes from the middle point to GND, and the middle point goes to GPIO0. In the light, the LDR's resistance drops and the middle voltage rises; in the dark, it falls. It is the circuit from the LDR lesson.
Here a key difference appears between the DevKit V1's ESP32 and the ESP32-C3. On the C3, only GPIO0 to GPIO4 read voltages through ADC1. GPIO5 belongs to ADC2, which MicroPython cannot open on this chip. That is why the LDR goes on GPIO0. And, between GPIO0 and GPIO4, GPIO2 is best avoided, since it is a chip configuration pin at startup.
The LED, with the 220 Ω resistor, goes on GPIO6. It receives PWM, so it can fade on and off gently.
A power detail: the PIR sits on 5 V and the LDR on 3.3 V. On the example circuit's breadboard, each one is connected to a different rail — 5 V on the top one and 3.3 V on the bottom one. Connecting the LDR to 5 V would push the middle point past the 3.6 V the pin can take.
Step 2: reading the sensors
The first program only reads and shows. Open it with the circuit: the example's LDR is in a 5 lux environment, like a bedroom at night with some street light, and the PIR is idle.
from machine import Pin, ADC
import time
ldr = ADC(Pin(0), atten=ADC.ATTN_11DB) # up to about 3.1 V on the C3
pir = Pin(1, Pin.IN)
for i in range(4):
mv = ldr.read_uv() // 1000
print('LDR %4d mV PIR %d' % (mv, pir.value()))
time.sleep(1)In the simulator, click the LDR and change the light to 300 lux (a lit room): the reading rises to close to 2.9 V. Click the PIR and tick "someone passing": the output goes to 1 and stays there for the hold time. Note down the LDR readings in a few situations — they will define the project's "dark" threshold.
Step 3: calibrating "dark"
What is dark? It depends on the place. The honest way is to measure: with the hallway light as you want it to be at night and in the daytime, write down the readings. In the simulator, 5 lux gave about 960 mV and 300 lux, about 2900 mV. A threshold between the two — 1200 mV — separates the situations well.
But a single threshold creates a problem in the late afternoon: when the light is very close to it, the reading oscillates and the project gets "in doubt", turning on and off. Two techniques solve this, the same ones from the battery monitor in the article batteries on the ESP32:
- average: read several times and take the average, as in the moving average lesson;
- hysteresis: one threshold to become "dark" (1200 mV) and another, higher one, to go back to "light" (1500 mV).
The example tests the hysteresis with a list of readings from a late afternoon.
ESCURECEU = 1200 # mV: below this, night has fallen
CLAREOU = 1500 # mV: it only becomes day again above this
leituras = [2900, 2400, 1700, 1300, 1190, 1230, 1180, 1250, 1210, 900, 600, 1400, 1450, 1520, 2100]
escuro = False
for mv in leituras:
antes = escuro
if not escuro and mv < ESCURECEU:
escuro = True
elif escuro and mv > CLAREOU:
escuro = False
print('%4d mV -> %s%s' % (mv, 'dark' if escuro else 'light', ' <- changed' if escuro != antes else ''))Without the hysteresis, the readings 1190, 1230, 1180 and 1250 would make the state flip four times in four readings. With it, it flips once at dusk and once at dawn.
The AI that remembers your life and lives on your computer
- Long-term memory with the source of every answer
- Works offline: your data stays on your PC
- Reads PDFs, slides, photos and meeting recordings
Essential plan free forever
Step 4: drawing the states
Before writing the main program, it is worth drawing what the light can be doing and what makes it change. There are three states:
- OFF: LED off; waits for dark and movement;
- ON: LED at the chosen brightness; each movement renews the time; with no movement for 20 s, it starts to fade out;
- FADING: the brightness drops a little at each step; if someone passes, it goes back to ON; if the brightness reaches zero, it becomes OFF.
And one special transition: if it gets light while the light is on (someone turned on the hallway lamp, or it is morning), it turns off at once. The diagram is the same kind of drawing as in the state machine lesson, and it is what shows the cases that a program written "off the top of your head" would forget — like what to do if someone passes while the light is fading out.
Step 5: the complete program
The program follows the diagram to the letter. A variable holds the state; at every 50 ms step, it reads the sensors and decides. The brightness uses PWM with duty_u16, as in the fade lesson: raising and lowering the brightness gradually is just changing the duty a little at each step.
To make the example fit in the simulator, the inputs come from a list of events (second, dark, movement) instead of the sensors, and the times are short: 2 s of waiting with no movement instead of 20 s. The circuit's LED is real: watch it turn on and off in the 2D view while the program runs.
from machine import Pin, PWM
from time import ticks_ms, ticks_diff, sleep_ms
led = PWM(Pin(6), freq=1000, duty_u16=0)
BRILHO = 40000 # from 0 to 65535: it need not be the maximum at night
SEM_MOVIMENTO_MS = 2000 # in the project, 20 s
PASSO = 1000 # how much the brightness changes every 50 ms: 2 s to rise or fall
# (second, is it dark?, is there movement?) — the "story" of a short night
historia = [(0, False, True), (1, True, False), (2, True, True), (3, True, False),
(5.6, True, True), (5.8, True, False), (8.8, False, False)]
estado, brilho, ultimo_mov = 'OFF', 0, 0
escuro = movimento = False
inicio = ticks_ms()
while ticks_diff(ticks_ms(), inicio) < 11000:
t = ticks_diff(ticks_ms(), inicio)
while historia and t >= historia[0][0] * 1000:
_, escuro, movimento = historia.pop(0)
anterior = estado
if movimento:
ultimo_mov = t
if estado == 'OFF' and escuro and movimento:
estado = 'ON'
elif estado == 'ON' and t - ultimo_mov >= SEM_MOVIMENTO_MS:
estado = 'FADING'
elif estado == 'FADING' and movimento:
estado = 'ON'
elif estado == 'FADING' and brilho == 0:
estado = 'OFF'
if not escuro and estado != 'OFF':
estado, brilho = 'OFF', 0 # it got light: turn off at once
alvo = BRILHO if estado == 'ON' else 0
if brilho < alvo:
brilho = min(alvo, brilho + PASSO)
elif brilho > alvo:
brilho = max(alvo, brilho - PASSO)
led.duty_u16(brilho)
if estado != anterior:
print('%5.1f s %-8s -> %s' % (t / 1000, anterior, estado))
sleep_ms(50)Read the output along with the story: at second 0 there is movement, but it is light — nothing happens. At second 2, dark and movement: the light turns on. It stays on until 2 s after the last movement and starts to fade out; shortly after, someone passes again during the fade, and the light comes back on. At the end, it starts to fade out once more, but it gets light before finishing — and it turns off at once.
For the version with real sensors, swap the list for the readings: movimento = pir.value() == 1 and the escuro from the hysteresis in step 3, with the average of a few LDR readings. Also set SEM_MOVIMENTO_MS to about 20 seconds.
A fine point about the PIR: the module itself already has a hold time, set on a trimpot, during which the output stays at 1 after the movement ends. The program's time adds to it. Leave the trimpot at the minimum and control the time through the program: it is easier to change and to explain.
Why turn on slowly
Turning on all at once would work, but a light that grows over a second or two bothers eyes used to the dark much less — and looks more "polished". There is a curious detail here: the eye does not perceive brightness in proportion to energy. The difference between a duty of 1 % and 5 % looks enormous; between 80 % and 100 %, it is barely noticeable. That is why, in many projects, the brightness rises along a curve instead of a straight line: small steps at the start and larger ones at the end. A simple way to test it is to square the brightness fraction: led.duty_u16(int(65535 * (fracao ** 2))). Try both versions in the simulator and on the bench and ask the class to say which looks more "linear" — it is a good experiment about perception.
The example's BRILHO is also not the maximum. At night, with adapted eyes, little light is enough to walk safely, and less brightness means less glare. Adjust it with the LED already in the place where it will stay.
Step 6: assembling and installing
- build the circuit on the breadboard following the image and test it with the step 2 program;
- save the complete program as
main.py; - on the real board, wait for the PIR to warm up: the lesson warns that, right after being powered on, it may trigger by itself for a while;
- choose the place: the PIR needs to "see" people passing, and the LDR needs to see the room's light — but not the LED's own light. If the LDR sees the LED, the night light turns on, the LDR thinks it got light and it turns off, in an endless cycle. Point the LED at the floor and the LDR at the ceiling, or separate the two with a shield;
- power it with a phone charger through USB-C.
Step 7: testing a whole night
A project that works for ten seconds on the desk may behave differently over a whole night. A simple test: have the program print to the Serial (or save to a file) each state change, with the time since the board turned on, and leave the light running in the hallway for a night. In the morning, look at the log:
- how many times did it turn on? Does it make sense with the number of times someone passed?
- did it turn on during the day? If so, the dark threshold is too high;
- did it turn on with nobody passing? It may be the PIR seeing a window, a heater or a pet;
- how long was it on in total? That number, multiplied by the LED's current, tells how much energy the light used.
This log turns "I think it works" into "I know how it works" — and it is the basis for tuning the program's numbers with arguments, not by guesswork.
Common problems
- It turns on in the daytime: the dark threshold is too high for the place. Measure again with the step 2 program and adjust.
- It flickers in the late afternoon: there is no hysteresis, or the gap between the two thresholds is too small.
- It turns on by itself at dawn: drafts of hot air and pets also move the PIR. Adjust the sensitivity on the module's other trimpot, and the position.
- It turns on and off nonstop: the LDR is seeing the LED (step 6, item 4).
Ideas to go further
- Brightness by time of day: with the board's clock set over the internet, the light can be dimmer in the small hours and brighter early in the evening.
- Log of passes: counting how many times someone passed at night and saving it to a file, as in the article storing data on the ESP32, can become an interesting chart for a statistics class.
- LED ring: swapping the LED for a NeoPixel ring, from the displays track, lets you choose the light's color.
What this project teaches
On the outside, it is a little light. On the inside, there is calibration ("dark" is measured, not guessed), filtering (average and hysteresis against oscillating readings), analog output with PWM and, above all, the discipline of drawing the states before programming. Almost every appliance at home — from the washing machine to the microwave — runs on a state machine similar to this one. A good exercise for the class is to pick one of these appliances and draw its diagram.
It is also worth noting what the project does not need: internet, an app, an account on any service. Everything happens inside the board, with three cheap sensors and a few dozen lines of Python. A lot of real "smart home" could be like this — simpler, cheaper and working even when the Wi-Fi goes down.
Where to go next
The lessons used are in the Analog tracks (LDR and moving average), Sensors (PIR), PWM (fade) and Time without freezing (state machine). The article The ESP32's ADC explains the ADC's attenuation and curve, and the pin map helps with the variations.
Sources
- ESP32-C3 ADC channels in MicroPython v1.29.0:
ports/esp32/machine_adc.c, as recorded in the simulator's chip profile; MicroPython documentation:machine.ADC. - ESP32-C3 Super Mini pinout: espboards.dev and Last Minute Engineers.
- How the HC-SR501 PIR and the LDR work: the lessons and the component datasheets, with the sources cited in them.
To practice in PyPlaca
Tracks
- Track 4: Analog: ADC, potentiometer and LDR
- Track 5: Sensors: temperature, distance and presence
- Track 3: PWM: brightness, color, sound and motion
- Track 7: Time without freezing
Lessons
- LDR: measuring light
- PIR: did someone walk by?
- Fade: turning on and off slowly
- State machine
- Moving average: calming the reading