Getting up at night is one of the most common times for older people to fall: the room is dark, they are sleepy, and switching on a bright ceiling light is dazzling. Care homes and hospitals often use night path lights instead: soft lights near the floor that switch on by themselves when someone gets out of bed. In this capstone you build one. A motion sensor notices someone moving, a light sensor makes sure it only works when it's dark, and six LEDs light up one after another like the lights along an airport runway, guiding the way. When nobody moves for a while, the lights fade out gently. The UNO also counts the night-time trips and remembers them, even after a power cut, so the family can notice if something changes.
What you'll learn
- How to plan a bigger project as a state machine
- How to combine a motion sensor and a light sensor into one decision
- How to fade six LEDs smoothly with PWM, one after another
- How to use hysteresis and timers so the light doesn't flicker at dusk
- How to save data in the EEPROM so it survives a power cut
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- PIR motion sensor HC-SR501 (UNO R3 Starter Kit)
- Photoresistor module (45-in-1 Sensor Kit)
- 6 LEDs, white or yellow, and 6 resistors, 220 Ω (UNO R3 Starter Kit)
- 1 push button (UNO R3 Starter Kit)
- A long strip of cardboard or a plank to mount the LEDs along the floor (optional)
- Breadboard and jumper wires (UNO R3 Starter Kit)
This is a learning project. It can make a dark room safer, but it is not a medical alarm or a certified safety device. The trip log is only a rough count: if someone you care for gets up at night much more often than before, it's worth mentioning to a doctor, but never use this project to decide anything about anyone's health.
Wiring

Build it step by step
Plug in all the parts first, then add the wires in this order. In each picture the new wires are bright and the ones from earlier steps are faded. Click a picture to see it larger.





| From | To |
|---|---|
| UNO 5V | Bottom + rail |
| UNO GND | Bottom − rail |
| UNO pins 3, 5, 6, 9, 10 and 11 | Each through a 220 Ω resistor to the long leg of one LED (LED 1 on pin 3 … LED 6 on pin 11) |
| Each LED's short leg | − rail |
| Photoresistor module S | UNO A0 |
| Photoresistor module + | + rail |
| Photoresistor module − | − rail |
| Push button, one leg | UNO pin 4 |
| Push button, the diagonally opposite leg | − rail |
| PIR sensor VCC | + rail |
| PIR sensor OUT | UNO pin 2 |
| PIR sensor GND | − rail |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
On the breadboard each LED sits in its own group of columns, with its resistor to the left, so they line up from LED 1 to LED 6. For the real path, solder or tape the LEDs along a strip of cardboard on long wires, LED 1 next to the bed and LED 6 towards the door.
Point the photoresistor away from the path LEDs, or shade it with a small paper tube. Otherwise the path light itself can make the sensor think it is daytime.
Set up the PIR sensor
- Turn the PIR's time knob fully anticlockwise (shortest time, about 3 seconds). The sketch does its own timing.
- Set the jumper to H (repeat trigger), so the output stays HIGH while someone keeps moving.
- Turn the sensitivity knob to the middle. Point the sensor where feet land when someone gets out of bed.
- After power-up, the PIR needs about a minute to settle. It may trigger by itself during that time.
The sketch
#include <EEPROM.h>
const int LEDS[6] = {3, 5, 6, 9, 10, 11}; // LED 1 (bed) ... LED 6 (door)
const int PIR_PIN = 2, BUTTON_PIN = 4, LIGHT_PIN = A0;
// ---- settings: tune these ----
const int DARK_BELOW = 300; // light reading that counts as dark (see the Serial Monitor)
const int LIGHT_ABOVE = 380; // a bit higher, so dusk doesn't flicker (hysteresis)
const bool DARK_IS_HIGH = false; // true if the reading goes UP when it gets darker
const int GLOW = 90; // night brightness, 0-255: gentle, not dazzling
const unsigned long STAY_ON_MS = 90000UL; // 90 s after the last movement
const unsigned long WAVE_GAP_MS = 180; // delay between LEDs in the wave
const unsigned long FADE_OUT_MS = 6000;
const unsigned long NIGHT_AFTER_MS = 60000UL; // dark this long: the night has started
const unsigned long MORNING_AFTER_MS = 600000UL;// light this long: the night is over
enum State { DAY, DARK_WAITING, WAVE_ON, ON, FADING };
const char* STATE_NAMES[] = {"DAY", "DARK_WAITING", "WAVE_ON", "ON", "FADING"};
State state = DAY;
int level[6]; // current brightness of each LED
unsigned long stateSince = 0, lastMotion = 0, darkSince = 0, lightSince = 0;
bool dark = false, night = false;
unsigned long nightStart = 0;
int tripsTonight = 0;
// EEPROM: byte 0 = next slot, bytes 1-7 = trips on the last 7 nights
const int LOG_NEXT = 0, LOG_START = 1, LOG_NIGHTS = 7;
void setAll(int value) {
for (int i = 0; i < 6; i++) { level[i] = value; analogWrite(LEDS[i], value); }
}
void goTo(State s) { state = s; stateSince = millis(); }
int lightReading() {
long sum = 0;
for (int i = 0; i < 8; i++) sum += analogRead(LIGHT_PIN);
int v = sum / 8;
return DARK_IS_HIGH ? 1023 - v : v; // now a low number always means dark
}
void saveNight() {
int next = EEPROM.read(LOG_NEXT);
if (next >= LOG_NIGHTS) next = 0;
EEPROM.update(LOG_START + next, min(tripsTonight, 255));
EEPROM.update(LOG_NEXT, (next + 1) % LOG_NIGHTS);
Serial.print("Night saved: ");
Serial.print(tripsTonight);
Serial.println(" trips");
}
void showLog() {
int next = EEPROM.read(LOG_NEXT);
if (next >= LOG_NIGHTS) next = 0;
Serial.println("Trips on the last 7 nights (oldest first):");
for (int i = 0; i < LOG_NIGHTS; i++) {
int v = EEPROM.read(LOG_START + (next + i) % LOG_NIGHTS);
Serial.print(v == 255 ? 0 : v); // 255 = never written
Serial.print(i < LOG_NIGHTS - 1 ? " " : "\n");
}
Serial.print("Tonight so far: ");
Serial.println(tripsTonight);
int last = EEPROM.read(LOG_START + (next + LOG_NIGHTS - 1) % LOG_NIGHTS);
if (last == 255) last = 0;
for (int i = 0; i < last; i++) { // blink LED 1 once per trip last night
analogWrite(LEDS[0], GLOW); delay(250);
analogWrite(LEDS[0], 0); delay(350);
}
analogWrite(LEDS[0], level[0]); // back to what it was
}
void setup() {
Serial.begin(9600);
pinMode(PIR_PIN, INPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
setAll(0);
Serial.println("Night path light. Press the button to see the log, hold it about 3 s for a test run.");
}
void updateDayNight(unsigned long now) {
int light = lightReading();
if (!dark && light < DARK_BELOW) { dark = true; darkSince = now; }
if (dark && light > LIGHT_ABOVE) { dark = false; lightSince = now; }
if (dark && !night && now - darkSince > NIGHT_AFTER_MS) {
night = true; nightStart = now; tripsTonight = 0;
Serial.println("Night started");
}
if (!dark && night && now - lightSince > MORNING_AFTER_MS) {
night = false;
saveNight();
}
}
void readButton(unsigned long now) {
static unsigned long downSince = 0;
static bool wasDown = false;
bool down = digitalRead(BUTTON_PIN) == LOW;
if (down && !wasDown) downSince = now;
if (!down && wasDown) {
unsigned long held = now - downSince;
if (held > 2000) { Serial.println("Test run"); lastMotion = now; goTo(WAVE_ON); }
else if (held > 40) showLog();
}
wasDown = down;
}
void loop() {
unsigned long now = millis();
updateDayNight(now);
readButton(now);
bool motion = digitalRead(PIR_PIN) == HIGH;
if (motion) lastMotion = now;
switch (state) {
case DAY:
case DARK_WAITING:
state = dark ? DARK_WAITING : DAY;
if (dark && motion) { // someone is up in the dark
if (night) {
tripsTonight++;
Serial.print("Trip ");
Serial.print(tripsTonight);
Serial.print(" at ");
Serial.print((now - nightStart) / 60000UL);
Serial.println(" min after dark");
}
goTo(WAVE_ON);
}
break;
case WAVE_ON: { // light the LEDs one after another
unsigned long t = now - stateSince;
for (int i = 0; i < 6; i++) {
long start = i * WAVE_GAP_MS;
int target = t < (unsigned long)start ? 0 : min(255L, (long)(t - start) * GLOW / 400);
target = min(target, GLOW);
if (target != level[i]) { level[i] = target; analogWrite(LEDS[i], target); }
}
if (level[5] >= GLOW) goTo(ON);
break;
}
case ON:
if (now - lastMotion > STAY_ON_MS) goTo(FADING);
break;
case FADING: { // fade out, the door end first
if (motion) { setAll(GLOW); goTo(ON); break; }
unsigned long t = now - stateSince;
for (int i = 0; i < 6; i++) {
long delayI = (5 - i) * 400L; // LED 6 starts fading first
long left = (long)FADE_OUT_MS - max(0L, (long)t - delayI);
int v = constrain(left * GLOW / (long)FADE_OUT_MS, 0, GLOW);
if (v != level[i]) { level[i] = v; analogWrite(LEDS[i], v); }
}
if (t > FADE_OUT_MS + 2000) { setAll(0); goTo(dark ? DARK_WAITING : DAY); }
break;
}
}
static unsigned long lastReport = 0;
if (now - lastReport > 5000) { // a status line every 5 s
lastReport = now;
Serial.print("light ");
Serial.print(lightReading());
Serial.print(dark ? " (dark)" : " (light)");
Serial.print(night ? " night" : "");
Serial.print(" state ");
Serial.println(STATE_NAMES[state]);
}
delay(10);
}
Try it
- Upload the sketch and open the Serial Monitor at 9600 baud. Every 5 seconds you see the light reading.
- Note the reading in normal room light, then cover the photoresistor with your hand and note it again. If the number went up when covered, set
DARK_IS_HIGHtotrue, upload, and note both readings again: they are now flipped, so covered gives the low number. Then setDARK_BELOWa little above your "covered" reading andLIGHT_ABOVEa bit higher still, and upload again. - Hold the button for about 3 seconds and let go: the test run lights the runway, LED 1 first, even in daylight.
- Cover the light sensor (or switch the room light off) and wave in front of the PIR. The LEDs light up one after another. Keep still for 90 seconds and they fade out, the door end first.
- To test the night log quickly, change
NIGHT_AFTER_MSto5000,MORNING_AFTER_MSto20000andSTAY_ON_MSto5000. Stay "dark" for 5 s, then make a few trips: wave, and wait until the lights are fully off before the next one. Then let light in for 20 s. "Night saved" appears. Press the button: LED 1 blinks once per trip, and the Serial Monitor shows the last 7 nights. Change the times back before you use it for real.
What you should see

How it works

A state machine. The light is always in exactly one state: DAY, DARK_WAITING, WAVE_ON, ON or FADING. Each state has its own small job and its own rule for moving on. Because nothing uses long delay()s, the UNO keeps watching the PIR, the light and the button the whole time. That's why movement during the fade-out brings the lights straight back.
Two sensors, one decision. The PIR answers "is someone moving?" and the photoresistor answers "is it dark?". The light only switches on when both are true. The light reading is averaged over 8 measurements, and there are two thresholds, DARK_BELOW and LIGHT_ABOVE, with a gap between them. This gap is called hysteresis: at dusk the reading wobbles around a single threshold, and without the gap the lamp would keep changing its mind.
The runway wave. In WAVE_ON, each LED starts brightening 180 ms after the one before it, so the light runs from the bed towards the door, showing the way. The brightness stops at GLOW (90 out of 255): enough to see the floor, not so much that it wakes everyone up. Our eyes adjust to the dark, so a dim light at night looks much brighter than it would by day.
Day, night and the log. A few dark minutes could just be a cloud or someone switching off a lamp for a moment. The sketch only decides that the night has started after a full minute of darkness, and that it has ended after ten minutes of light. (If someone leaves a bright light on for ten minutes in the night, the log starts a new "night" afterwards; make MORNING_AFTER_MS longer if that happens.) Trips are only counted during the night. In the morning the count is written to the EEPROM, a small memory inside the UNO that keeps its contents without power. Seven bytes store the last seven nights in a ring: the oldest night is overwritten first. EEPROM.update() only writes when the value has changed, because each EEPROM byte can only be written about 100,000 times.
Try this next
- Add the passive buzzer: if someone sets off the light but there is no movement at all for 20 minutes during the night, play a gentle tune to check they are OK.
- Use an ESP32 instead of the UNO and send the morning trip count to the family's phones with ntfy, like the fall detector lesson does.
- Make the wave run both ways: a second PIR near the door lights the runway from LED 6 back to LED 1 for the way back to bed.
- Make the brightness depend on how dark it is: a little brighter at dusk, dimmer in the middle of the night.
Troubleshooting
-
The lights come on in daylight: the light thresholds are the wrong way round. Check the readings in the Serial Monitor and set
DARK_IS_HIGHandDARK_BELOWagain. - The lights switch on by themselves: the PIR is still warming up, or it sees a heater, a window or a pet. Wait a minute after power-up, lower its sensitivity, and point it at the floor.
- One LED stays dark: it may be the wrong way round (the long leg must go to the resistor), or its resistor isn't in the same column as the LED's long leg.
- The button does nothing: it must sit across the middle gap, with one leg on pin 4 and the diagonally opposite leg on the − rail.
- The log shows strange numbers: a brand-new UNO's EEPROM is filled with 255, which the sketch shows as 0. If old numbers from another project appear, they will be replaced night by night.