Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| ESP32 DevKit V1 (30-pin) | 1 | ₹380 | Buy ESP32 DevKit V1 (30-pin) |
| 28BYJ-48 5 V stepper + ULN2003 driver board | 1 | ₹150 | Buy 28BYJ-48 5 V stepper + ULN2003 driver board |
| LDR light sensor module (VCC, GND, DO, AO) | 1 | ₹40 | Buy LDR light sensor module (VCC, GND, DO, AO) |
| Cord pulley / 3D-printed spool | 1 | ₹100 | Buy Cord pulley / 3D-printed spool |
| Jumper wires | 1 | ₹30 | Buy Jumper wires |
| 5 V 2 A adapter for the stepper (DC jack to wires) | 1 | ₹150 | Buy 5 V 2 A adapter for the stepper (DC jack to wires) |
| USB cable + 5 V USB charger for the ESP32 | 1 | ₹120 | Buy USB cable + 5 V USB charger for the ESP32 |
| Total | ₹970 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| ESP32 DevKit V1 | Part · pin | Wire | Notes |
|---|---|---|---|
| D14 | 28BYJ-48 stepper motor + ULN2003 driver · IN1 | orange | |
| D27 | 28BYJ-48 stepper motor + ULN2003 driver · IN2 | yellow | |
| D26 | 28BYJ-48 stepper motor + ULN2003 driver · IN3 | green | |
| D25 | 28BYJ-48 stepper motor + ULN2003 driver · IN4 | blue | |
| GND | 28BYJ-48 stepper motor + ULN2003 driver · - | black | Common ground with the stepper adapter |
| 5 V 2 A adapter (stepper) · +5V | 28BYJ-48 stepper motor + ULN2003 driver · + | red | ~240 mA while stepping — kept off the ESP32’s USB supply |
| 5 V 2 A adapter (stepper) · GND | 28BYJ-48 stepper motor + ULN2003 driver · - | black | |
| 3V3 | LDR light sensor module · VCC | red | 3.3 V so AO never exceeds the ADC’s range |
| GND | LDR light sensor module · GND | black | |
| D34 | LDR light sensor module · AO | purple |
Board quirks: The stepper has its own 5 V adapter; only its ground joins the ESP32, so motor current never flows through the board. On most LDR modules AO goes <em>down</em> as light goes up; the sketch inverts it so “brighter = bigger”. The 28BYJ-48 is geared slowly and has a little backlash — great for curtains, not for precise positioning. The sketch assumes the curtain starts closed. A limit switch is the proper fix (see upgrades).
Code
// Stepper curtain opener — ESP32 DevKit V1 + ULN2003 driver + 28BYJ-48 stepper + LDR module.
// Opens the curtain when it gets bright and closes it at dusk, with a gap so it doesn't hunt.
// ULN2003 IN1–IN4 → D14, D27, D26, D25. LDR module AO → D34 (ADC1).
constexpr uint8_t COIL[4] = {14, 27, 26, 25};
constexpr uint8_t LDR_PIN = 34;
// 28BYJ-48 in half-step mode: about 4096 steps per output-shaft turn.
constexpr long CURTAIN_STEPS = 4096L * 3; // three turns of the shaft — adjust for your curtain
constexpr int OPEN_ABOVE = 2600; // raw ADC (0–4095); on these modules brighter = LOWER, see quirks
constexpr int CLOSE_BELOW = 1600;
const uint8_t HALF_STEP[8][4] = {
{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0},
{0, 0, 1, 0}, {0, 0, 1, 1}, {0, 0, 0, 1}, {1, 0, 0, 1},
};
bool isOpen = false;
int phase = 0;
void coilsOff() {
for (uint8_t p : COIL) digitalWrite(p, LOW); // no holding current: the motor stays cool
}
void move(long steps) {
int dir = steps > 0 ? 1 : -1;
for (long i = 0; i < labs(steps); i++) {
phase = (phase + dir + 8) % 8;
for (int c = 0; c < 4; c++) digitalWrite(COIL[c], HALF_STEP[phase][c]);
delayMicroseconds(1200); // ~1.2 ms per half-step is reliable on 5 V
}
coilsOff();
}
int lightLevel() {
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN);
return 4095 - (sum / 16); // invert so that brighter = higher
}
void setup() {
for (uint8_t p : COIL) pinMode(p, OUTPUT);
coilsOff();
analogReadResolution(12);
Serial.begin(115200);
Serial.println("Curtain starts CLOSED — set it closed by hand before power-up");
}
void loop() {
int light = lightLevel();
Serial.printf("Light %d (%s)\n", light, isOpen ? "open" : "closed");
if (!isOpen && light > OPEN_ABOVE) {
Serial.println("Morning — opening");
move(CURTAIN_STEPS);
isOpen = true;
} else if (isOpen && light < CLOSE_BELOW) {
Serial.println("Dusk — closing");
move(-CURTAIN_STEPS);
isOpen = false;
}
delay(5000);
}// Stepper curtain opener — ESP32 DevKit V1 + ULN2003 driver + 28BYJ-48 stepper + LDR module.
// Opens the curtain when it gets bright and closes it at dusk, with a gap so it doesn't hunt.
// ULN2003 IN1–IN4 → D14, D27, D26, D25. LDR module AO → D34 (ADC1).
constexpr uint8_t COIL[4] = {14, 27, 26, 25};
constexpr uint8_t LDR_PIN = 34;
// 28BYJ-48 in half-step mode: about 4096 steps per output-shaft turn.
constexpr long CURTAIN_STEPS = 4096L * 3; // three turns of the shaft — adjust for your curtain
constexpr int OPEN_ABOVE = 2600; // raw ADC (0–4095); on these modules brighter = LOWER, see quirks
constexpr int CLOSE_BELOW = 1600;
const uint8_t HALF_STEP[8][4] = {
{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0},
{0, 0, 1, 0}, {0, 0, 1, 1}, {0, 0, 0, 1}, {1, 0, 0, 1},
};
bool isOpen = false;
int phase = 0;
void coilsOff() {
for (uint8_t p : COIL) digitalWrite(p, LOW); // no holding current: the motor stays cool
}
void move(long steps) {
int dir = steps > 0 ? 1 : -1;
for (long i = 0; i < labs(steps); i++) {
phase = (phase + dir + 8) % 8;
for (int c = 0; c < 4; c++) digitalWrite(COIL[c], HALF_STEP[phase][c]);
delayMicroseconds(1200); // ~1.2 ms per half-step is reliable on 5 V
}
coilsOff();
}
int lightLevel() {
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN);
return 4095 - (sum / 16); // invert so that brighter = higher
}
void setup() {
for (uint8_t p : COIL) pinMode(p, OUTPUT);
coilsOff();
analogReadResolution(12);
Serial.begin(115200);
Serial.println("Curtain starts CLOSED — set it closed by hand before power-up");
}
void loop() {
int light = lightLevel();
Serial.printf("Light %d (%s)\n", light, isOpen ? "open" : "closed");
if (!isOpen && light > OPEN_ABOVE) {
Serial.println("Morning — opening");
move(CURTAIN_STEPS);
isOpen = true;
} else if (isOpen && light < CLOSE_BELOW) {
Serial.println("Dusk — closing");
move(-CURTAIN_STEPS);
isOpen = false;
}
delay(5000);
}Compiled for esp32 on esp32 3.3.12 · 3 Nov 2026
How it works
The 28BYJ-48 is a small geared stepper motor: it moves in tiny exact steps, so the sketch can open the curtain by the same amount every morning without any position sensor. The ULN2003 board is just four transistor switches that let the ESP32’s 3.3 V pins drive the motor’s 5 V coils.
The sketch drives the coils in half-steps — eight patterns per cycle — which is smoother and quieter than full steps. About 4,096 half-steps turn the output shaft once; three turns is the starting value for a typical curtain cord.
An LDR module measures daylight. Opening and closing use two different light levels (hysteresis), so a passing cloud at dusk doesn’t make the curtain twitch open and shut.
After every move the coils are switched off. The gearbox holds the curtain in place, the motor stays cool and the ESP32 saves power.
Troubleshooting
The motor buzzes but doesn’t turn
The coil order is wrong or the steps are too fast. Check IN1–IN4 go to D14, D27, D26, D25 in that order, and keep the 1.2 ms delay.
It opens and closes repeatedly at dusk
The open and close thresholds are too close together. Widen the gap between OPEN_ABOVE and CLOSE_BELOW.
The motor gets hot
The sketch turns the coils off after each move. If yours is hot anyway, check for a stuck pulley.
Upgrades
- Add limit switches at both ends so it always knows where the curtain is.
- Add a web page to open and close it from your phone.
- Use the time (NTP) as well as light — “open at 7 am or at sunrise, whichever is later”.



